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/.clang-ignore b/.clang-ignore
new file mode 100644
index 0000000..5b7b6ae
--- /dev/null
+++ b/.clang-ignore
@@ -0,0 +1 @@
+./ncsid/src/platforms
diff --git a/meson.build b/meson.build
index c9d8c86..3ca0866 100644
--- a/meson.build
+++ b/meson.build
@@ -19,3 +19,5 @@
 if get_option('tests').auto()
   tests_str = 'auto'
 endif
+
+subproject('ncsid', default_options: 'tests=' + tests_str)
diff --git a/ncsid/README.md b/ncsid/README.md
new file mode 100644
index 0000000..84dc482
--- /dev/null
+++ b/ncsid/README.md
@@ -0,0 +1,3 @@
+# NC-SI Daemon for gBMC
+
+* [Internals](doc/ncsid_internals.md)
diff --git a/ncsid/doc/ncsid_arch.dot b/ncsid/doc/ncsid_arch.dot
new file mode 100644
index 0000000..7edc8a3
--- /dev/null
+++ b/ncsid/doc/ncsid_arch.dot
@@ -0,0 +1,60 @@
+digraph {
+	node [shape="box"];
+	subgraph {
+		node [shape=plaintext];
+
+		ncsid [label=<
+			<table>
+				<tr>
+					<td bgcolor="lightblue" colspan="2" port="config">net::ConfigBase</td>
+					<td port="sockio" bgcolor="lightblue">net::SockIO</td>
+				</tr>
+				<tr><td bgcolor="lightblue" colspan="3">ncsi::StateMachine</td></tr>
+				<tr>
+					<td bgcolor="black"><font color="white">L2 FSM</font></td>
+					<td bgcolor="black"><font color="white">L3/4 FSM</font></td>
+					<td bgcolor="black"><font color="white">Test FSM</font></td>
+				</tr>
+			</table>
+		>];
+	}
+
+	subgraph notes {
+		node [shape="note" style="filled"];
+		core [label="NC-SId Core" fillcolor="lightblue"];
+		hardware [label="Hardware" fillcolor="limegreen"];
+		external [label="External Components" color="magenta" fillcolor="white"];
+		ec [label="EC" fillcolor="black" fontcolor="white"];
+	} -> NIC [style="invis"];
+
+	subgraph external {
+		node [color="magenta"];
+		p_networkd [label="phosphord-networkd"];
+		systemd;
+		d_bus [shape="doublecircle" label="DBus"];
+	}
+
+	subgraph core_comps {
+		node [fillcolor="lightblue" style="filled"];
+		p_config [label="net::PhosphorConfig"];
+		ncsi_sockio [label="ncsi::SockIO"];
+		net_ifacebase [label="net::IFaceBase"];
+		net_iface [label="net::IFace"];
+
+		net_iface -> net_ifacebase [arrowhead="diamond"];
+		ncsi_sockio -> net_iface [label="bind"];
+		ncsid;
+	}
+
+	NIC [shape="tab" fillcolor="limegreen" style="filled"];
+
+	ncsi_sockio -> ncsid:sockio [arrowhead="diamond"];
+	NIC -> ncsi_sockio [dir="both" label="NC-SI Cable" color="limegreen"];
+
+	p_config -> ncsid:config [arrowhead="diamond"];
+	p_config -> p_networkd [style="dashed"];
+	p_config -> d_bus;
+	p_networkd -> d_bus [dir="both"];
+	d_bus -> systemd;
+	p_networkd -> systemd [style="dashed"];
+}
diff --git a/ncsid/doc/ncsid_arch.png b/ncsid/doc/ncsid_arch.png
new file mode 100644
index 0000000..1e26622
--- /dev/null
+++ b/ncsid/doc/ncsid_arch.png
Binary files differ
diff --git a/ncsid/doc/ncsid_internals.md b/ncsid/doc/ncsid_internals.md
new file mode 100644
index 0000000..a85e6f6
--- /dev/null
+++ b/ncsid/doc/ncsid_internals.md
@@ -0,0 +1,106 @@
+# NC-SId Internals
+
+__NOTE__: This documents describes the internal architecture of NC-SId daemon.
+However, it is meant to be used as a guide for understanding the code, not on
+its own. Some details are intentionally omitted.
+
+![Internals Diagram](ncsid_arch.png)
+
+In the diagram above the components are split into four groups:
+
+* __NC-SId Core__. These are new components implemented in NC-SId.
+
+* __Hardware__. External hardware components, in this case, the NIC.
+
+* __EC__. This is the code borrowed from EC. The three state machines are
+  pretty much copied from EC code.
+
+* __External Components__. These are external services/daemons NC-SIs interacts
+  with.
+
+Let's look into their details.
+
+## NIC
+
+In the NIC — NC-SId interactions, NIC acts as a server, replying to NC-SId
+requests and NC-SId itself acts as a client, sending those requests.  Thus,
+there is no state in NIC (server), but there is a state in NC-SId (client).
+
+## EC State Machines
+
+NC-SId reuses the state machines from EC. They are treated like black boxes.
+They are C functions with simple interface: the state machine is given incoming
+NC-SI command buffer (reply from the NIC) and returns the buffer that needs to
+be sent to the NIC (the next command).
+
+### L2 FSM
+
+This state machine performs basic configuration of the NC-SI comm channel and
+also reads the MAC Address of the NIC.
+
+### L3/4 FSM
+
+Once BMC's network is configured, this state machine sets up filters in the NIC.
+
+### Test FSM
+
+This state machine periodically tests NC-SI connection with the NIC, verifies
+filters and queries the NIC state (hostless or host-based). If it ever fails,
+all state machines restart, which means that NC-SI in the NIC is also reset and
+reconfigured.
+
+---
+
+In addition to the buffer there are parameters that provide information which is
+not a part of EC state machines' state:
+
+* State Parameters. These structures are allocated outside of EC State Machines,
+  but their content is fully managed by EC State Machines.
+* MAC Address. For L2 FSM this parameter is _OUT_.
+* IP Address (only for L3/4 FSM and Test FSM) for setting up and verifying
+  filteres. If set to zero, the NIC filter won't check for IP address.
+* TCP Port (only for L3/4 FSM and Test FSM) for setting up and verifying
+  filters.
+
+In the initial state the command buffer (reply from the NIC) is empty. When
+there is nothing more to send to the NIC, i.e. that particular state machine is
+done, it returns empty buffer.
+
+## External Components
+
+NC-SId uses `phosphord-networkd` to configure the BMC's network (MAC Address).
+In turn, `phosphord-networkd` uses `systemd`. Their interactions go through
+`DBus`.
+
+## NC-SId Core
+
+### ncsi::StateMachine
+
+This component coordinates the interaction between EC State Machines and is also
+heavily based on EC code. It uses `net::SockIO` interface to interact with the
+NIC and `net::ConfigBase` interface to set/query MAC Address.
+
+### net::PhosphorConfig
+
+Implements `net::ConfigBase` and makes calls to `phosphord-networkd` via `DBus`
+to get/set MAC Address.
+
+### ncsi::SockIO
+
+Implements `net::SockIO` and sends NC-SI commands to the NIC through raw Unix
+socket. That socket is configured using `net::IFace` component, which represents
+the network interface (think ethX). To simplify testing, the abstract
+`net::IFaceBase` interface is introduced.
+
+---
+
+## Unit Testing
+
+![Test infrastructure](ncsid_test_arch.png)
+
+To allow some fairly sophisticated unit-tests, EC State Machines as well as
+`ncsi::StateMachine` component only interact with the outside world using
+`net::SockIO` and `net::ConfigBase` interfaces. This makes it easy to mock them.
+
+The most complicated part of these tests is `mock::NIC`, which acts as a NC-SI
+server, replying to NC-SI requests coming from NC-SI State Machines.
diff --git a/ncsid/doc/ncsid_test_arch.dot b/ncsid/doc/ncsid_test_arch.dot
new file mode 100644
index 0000000..081d5d6
--- /dev/null
+++ b/ncsid/doc/ncsid_test_arch.dot
@@ -0,0 +1,38 @@
+digraph {
+	node [shape="box"];
+	subgraph {
+		node [shape=plaintext];
+
+		ncsid [label=<
+			<table>
+				<tr><td port="sockio" bgcolor="lightblue">net::SockIO</td><td bgcolor="lightblue" colspan="2" port="config">net::ConfigBase</td></tr>
+				<tr><td bgcolor="lightblue" colspan="3">ncsi::StateMachine</td></tr>
+				<tr>
+					<td bgcolor="black"><font color="white">L2 FSM</font></td>
+					<td bgcolor="black"><font color="white">L3/4 FSM</font></td>
+					<td bgcolor="black"><font color="white">Test FSM</font></td>
+				</tr>
+			</table>
+		>];
+	}
+
+	subgraph notes {
+		node [shape="note" style="filled"];
+		core [label="NC-SId Core" fillcolor="lightblue"];
+		ec [label="EC" fillcolor="black" fontcolor="white"];
+		mock [label="Mocks" fillcolor="beige"];
+	}
+
+	p_config [label="MockConfig" style="filled" fillcolor="beige"];
+
+	ncsi_sockio [style="filled" fillcolor="beige" label=<
+		<table border="0">
+			<tr><td align="left">NICConnection</td></tr>
+			<tr><td border="1">mock::NIC</td></tr>
+		</table>
+	>];
+
+	ncsi_sockio -> ncsid:sockio [arrowhead="diamond"];
+
+	p_config -> ncsid:config [arrowhead="diamond"];
+}
diff --git a/ncsid/doc/ncsid_test_arch.png b/ncsid/doc/ncsid_test_arch.png
new file mode 100644
index 0000000..6c3ee64
--- /dev/null
+++ b/ncsid/doc/ncsid_test_arch.png
Binary files differ
diff --git a/ncsid/meson.build b/ncsid/meson.build
new file mode 100644
index 0000000..508a4ce
--- /dev/null
+++ b/ncsid/meson.build
@@ -0,0 +1,20 @@
+project(
+  'gbmc-ncsid',
+  ['cpp', 'c'],
+  version: '0.1',
+  meson_version: '>=0.53.2',
+  default_options: [
+    'warning_level=3',
+    'werror=true',
+    'cpp_std=c++17',
+    'c_std=c11',
+    'tests=' + (meson.is_subproject() ? 'disabled' : 'auto'),
+  ])
+
+subdir('src')
+
+build_tests = get_option('tests')
+
+if not build_tests.disabled()
+  subdir('test')
+endif
diff --git a/ncsid/meson_options.txt b/ncsid/meson_options.txt
new file mode 100644
index 0000000..0fc2767
--- /dev/null
+++ b/ncsid/meson_options.txt
@@ -0,0 +1 @@
+option('tests', type: 'feature', description: 'Build tests')
diff --git a/ncsid/src/common_defs.h b/ncsid/src/common_defs.h
new file mode 100644
index 0000000..70f51b8
--- /dev/null
+++ b/ncsid/src/common_defs.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#define RETURN_IF_ERROR(expr, msg)                                             \
+    do                                                                         \
+    {                                                                          \
+        int _ret = (expr);                                                     \
+        if (_ret < 0)                                                          \
+        {                                                                      \
+            std::perror(msg);                                                  \
+            return _ret;                                                       \
+        }                                                                      \
+    } while (0)
diff --git a/ncsid/src/dhcp4@.service.in b/ncsid/src/dhcp4@.service.in
new file mode 100644
index 0000000..de94729
--- /dev/null
+++ b/ncsid/src/dhcp4@.service.in
@@ -0,0 +1,14 @@
+[Unit]
+Description=DHCPv4 Updater
+Wants=mapper-wait@-xyz-openbmc_project-network-%i.service
+After=mapper-wait@-xyz-openbmc_project-network-%i.service
+Requisite=nic-hostless@%i.target
+After=nic-hostless@%i.target
+BindsTo=nic-hostless@%i.target
+ConditionPathExists=!/run/dhcp4.done
+
+[Service]
+KillMode=mixed
+Restart=on-failure
+ExecStart=@/usr/bin/env udhcpc -V gBMC -f -q -i %I -s @SCRIPT@
+SyslogIdentifier=dhcp4@%I
diff --git a/ncsid/src/dhcp6@.service.in b/ncsid/src/dhcp6@.service.in
new file mode 100644
index 0000000..5e03458
--- /dev/null
+++ b/ncsid/src/dhcp6@.service.in
@@ -0,0 +1,14 @@
+[Unit]
+Description=DHCPv6 Updater
+Wants=mapper-wait@-xyz-openbmc_project-network-%i.service
+After=mapper-wait@-xyz-openbmc_project-network-%i.service
+Requisite=nic-hostless@%i.target
+After=nic-hostless@%i.target
+BindsTo=nic-hostless@%i.target
+ConditionPathExists=!/run/dhcp6.done
+
+[Service]
+KillMode=mixed
+Restart=on-failure
+ExecStart=@/usr/bin/env udhcpc6 -f -q -O bootfile_url -O bootfile_param -i %I -s @SCRIPT@
+SyslogIdentifier=dhcp6@%I
diff --git a/ncsid/src/meson.build b/ncsid/src/meson.build
new file mode 100644
index 0000000..2e23322
--- /dev/null
+++ b/ncsid/src/meson.build
@@ -0,0 +1,119 @@
+ncsid_headers = include_directories('.')
+
+fmt_dep = dependency('fmt', required: false)
+if not fmt_dep.found()
+  fmt_proj = import('cmake').subproject(
+    'fmt',
+    cmake_options: [
+      '-DCMAKE_POSITION_INDEPENDENT_CODE=ON',
+      '-DMASTER_PROJECT=OFF'
+    ],
+    required: false)
+  assert(fmt_proj.found(), 'fmtlib is required')
+  fmt_dep = fmt_proj.dependency('fmt')
+endif
+
+ncsid_deps = [
+  fmt_dep,
+  dependency('sdbusplus', fallback: ['sdbusplus', 'sdbusplus_dep']),
+  dependency('stdplus', fallback: ['stdplus', 'stdplus']),
+]
+
+ncsid_lib = static_library(
+  'ncsid',
+  [
+    'net_config.cpp',
+    'net_iface.cpp',
+    'net_sockio.cpp',
+    'ncsi_sockio.cpp',
+    'ncsi_state_machine.cpp',
+    'platforms/nemora/portable/ncsi_fsm.c',
+    'platforms/nemora/portable/ncsi_client.c',
+    'platforms/nemora/portable/ncsi_server.c',
+  ],
+  include_directories: ncsid_headers,
+  implicit_include_directories: false,
+  dependencies: ncsid_deps)
+
+ncsid = declare_dependency(
+  dependencies: ncsid_deps,
+  include_directories: ncsid_headers,
+  link_with: ncsid_lib)
+
+executable(
+  'ncsid',
+  'ncsid.cpp',
+  implicit_include_directories: false,
+  dependencies: ncsid,
+  install: true,
+  install_dir: get_option('libexecdir'))
+
+normalize_ip = executable(
+  'normalize_ip',
+  'normalize_ip.c',
+  implicit_include_directories: false,
+  install: true)
+
+normalize_mac = executable(
+  'normalize_mac',
+  'normalize_mac.c',
+  implicit_include_directories: false,
+  install: true)
+
+install_data(
+  'ncsid_udhcpc4.script',
+  'ncsid_udhcpc6.script',
+  install_mode: 'rwxr-xr-x',
+  install_dir: get_option('libexecdir'))
+
+install_data(
+  'ncsid_lib.sh',
+  install_mode: 'rw-r--r--',
+  install_dir: get_option('libexecdir'))
+
+install_data(
+  'update_static_neighbors.sh',
+  install_mode: 'rwxr-xr-x',
+  install_dir: get_option('libexecdir'))
+
+systemd = dependency('systemd')
+systemunitdir = systemd.get_pkgconfig_variable('systemdsystemunitdir')
+
+libexecdir = get_option('prefix') / get_option('libexecdir')
+
+configure_file(
+  configuration: {'BIN': libexecdir / 'ncsid'},
+  input: 'ncsid@.service.in',
+  output: 'ncsid@.service',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
+
+configure_file(
+  configuration: {'BIN': libexecdir / 'update_static_neighbors.sh'},
+  input: 'update-static-neighbors@.service.in',
+  output: 'update-static-neighbors@.service',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
+
+configure_file(
+  configuration: {
+    'SCRIPT': libexecdir / 'ncsid_udhcpc4.script'},
+  input: 'dhcp4@.service.in',
+  output: 'dhcp4@.service',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
+
+configure_file(
+  configuration: {
+    'SCRIPT': libexecdir / 'ncsid_udhcpc6.script'},
+  input: 'dhcp6@.service.in',
+  output: 'dhcp6@.service',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
+
+install_data(
+  'nic-hostful@.target',
+  'nic-hostless@.target',
+  'update-static-neighbors@.timer',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
diff --git a/ncsid/src/ncsi_sockio.cpp b/ncsid/src/ncsi_sockio.cpp
new file mode 100644
index 0000000..df0c4fc
--- /dev/null
+++ b/ncsid/src/ncsi_sockio.cpp
@@ -0,0 +1,82 @@
+#include "ncsi_sockio.h"
+
+#include "common_defs.h"
+#include "net_iface.h"
+
+#include <linux/filter.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <cstdio>
+#include <cstring>
+
+namespace ncsi
+{
+
+int SockIO::init()
+{
+    RETURN_IF_ERROR(sockfd_ = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)),
+                    "ncsi::SockIO::init() failed");
+    return 0;
+}
+
+int SockIO::bind_to_iface(const net::IFaceBase& iface)
+{
+    iface.set_sock_flags(sockfd_, IFF_PROMISC);
+
+    std::memset(&sock_addr_, 0, sizeof(sock_addr_));
+
+    sock_addr_.sll_family = AF_PACKET;
+    sock_addr_.sll_protocol = htons(ETH_P_ALL);
+
+    RETURN_IF_ERROR(iface.bind_sock(sockfd_, &sock_addr_),
+                    "ncsi::SockIO::bind_to_iface failed");
+
+    return 0;
+}
+
+/**
+ * Drops VLAN tagged packets from a socket
+ *
+ * ld vlant
+ * jneq #0, drop
+ * ld proto
+ * jneq #0x88f8, drop
+ * ret #-1
+ * drop: ret #0
+ */
+struct sock_filter vlan_remove_code[] = {
+    {0x20, 0, 0, 0xfffff02c}, {0x15, 0, 3, 0x00000000},
+    {0x20, 0, 0, 0xfffff000}, {0x15, 0, 1, 0x000088f8},
+    {0x6, 0, 0, 0xffffffff},  {0x6, 0, 0, 0x00000000}};
+
+struct sock_fprog vlan_remove_bpf = {
+    std::size(vlan_remove_code),
+    vlan_remove_code,
+};
+
+int SockIO::filter_vlans()
+{
+    return setsockopt(sockfd_, SOL_SOCKET, SO_ATTACH_FILTER, &vlan_remove_bpf,
+                      sizeof(vlan_remove_bpf));
+}
+
+int SockIO::recv(void* buf, size_t maxlen)
+{
+    struct pollfd sock_pollfd
+    {
+        sockfd_, POLLIN | POLLPRI, 0
+    };
+
+    int ret = poll(&sock_pollfd, 1, kpoll_timeout_);
+    if (ret > 0)
+    {
+        return ::recv(sockfd_, buf, maxlen, 0);
+    }
+
+    return ret;
+}
+
+} // namespace ncsi
diff --git a/ncsid/src/ncsi_sockio.h b/ncsid/src/ncsi_sockio.h
new file mode 100644
index 0000000..8d5aaef
--- /dev/null
+++ b/ncsid/src/ncsi_sockio.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "net_iface.h"
+#include "net_sockio.h"
+
+#include <sys/socket.h>
+
+#include <cstddef>
+#include <cstring>
+
+namespace ncsi
+{
+
+class SockIO : public net::SockIO
+{
+  public:
+    SockIO() = default;
+
+    explicit SockIO(int sockfd) : net::SockIO(sockfd)
+    {}
+
+    // This function creates a raw socket and initializes sockfd_.
+    // If the default constructor for this class was used,
+    // this function MUST be called before the object can be used
+    // for anything else.
+    int init();
+
+    // Since raw packet socket is used for NC-SI, it needs to be bound
+    // to the interface. This function needs to be called after init,
+    // before the socket it used for communication.
+    int bind_to_iface(const net::IFaceBase& iface);
+
+    // Applies a filter to the interface to ignore VLAN tagged packets
+    int filter_vlans();
+
+    // Non-blocking version of recv. Uses poll with timeout.
+    int recv(void* buf, size_t maxlen) override;
+
+  private:
+    struct sockaddr_ll sock_addr_;
+    const int kpoll_timeout_ = 10;
+};
+
+} // namespace ncsi
diff --git a/ncsid/src/ncsi_state_machine.cpp b/ncsid/src/ncsi_state_machine.cpp
new file mode 100644
index 0000000..6eabf5d
--- /dev/null
+++ b/ncsid/src/ncsi_state_machine.cpp
@@ -0,0 +1,381 @@
+#include "ncsi_state_machine.h"
+
+#include "common_defs.h"
+#include "platforms/nemora/portable/default_addresses.h"
+#include "platforms/nemora/portable/ncsi_fsm.h"
+
+#include <arpa/inet.h>
+#include <netinet/ether.h>
+#include <unistd.h>
+
+#include <chrono>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <thread>
+
+#define ETHER_NCSI 0x88f8
+
+#define CPRINTF(...) fprintf(stderr, __VA_ARGS__)
+
+#ifdef NCSID_VERBOSE_LOGGING
+#define DEBUG_PRINTF printf
+#else
+#define DEBUG_PRINTF(...)
+#endif
+
+namespace ncsi
+{
+
+namespace
+{
+
+const char kStateFormat[] = "l2_config=%d/%d l3l4_config=%d/%d test=%d/%d";
+// This assumes that the number of states is < 100, so each number
+// in the format above does not take more than two characters to represent,
+// thus %d (two characters) is substituted for the number which is also
+// two characters max.
+constexpr auto kStateFormatLen = sizeof(kStateFormat);
+
+void snprintf_state(char* buffer, int size, const ncsi_state_t* state)
+{
+    (void)snprintf(buffer, size, kStateFormat, state->l2_config_state,
+                   NCSI_STATE_L2_CONFIG_END, state->l3l4_config_state,
+                   NCSI_STATE_L3L4_CONFIG_END, state->test_state,
+                   NCSI_STATE_TEST_END);
+}
+
+void print_state(const ncsi_state_t& state)
+{
+    (void)state;
+    DEBUG_PRINTF(kStateFormat, state.l2_config_state, NCSI_STATE_L2_CONFIG_END,
+                 state.l3l4_config_state, NCSI_STATE_L3L4_CONFIG_END,
+                 state.test_state, NCSI_STATE_TEST_END);
+    DEBUG_PRINTF(" restart_delay_count=%d\n", state.restart_delay_count);
+}
+
+const uint8_t echo_pattern[NCSI_OEM_ECHO_PATTERN_SIZE] = {
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+    0xFF, 0xFF, 0xFF, 0xFF, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A,
+    0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0xA5, 0x12, 0x34, 0x56, 0x78,
+    0x9A, 0xBC, 0xDE, 0xF0, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
+
+} // namespace
+
+void StateMachine::reset()
+{
+    std::memset(&ncsi_state_, 0, sizeof(ncsi_state_));
+    ncsi_state_.restart_delay_count = NCSI_FSM_RESTART_DELAY_COUNT - 1;
+    network_debug_.ncsi.test.max_tries = MAX_TRIES;
+    // This needs to be initialized in the firmware.
+    network_debug_.ncsi.test.ch_under_test = 0;
+    network_debug_.ncsi.oem_filter_disable = false;
+
+    network_debug_.ncsi.pending_stop = false;
+    network_debug_.ncsi.enabled = true;
+    network_debug_.ncsi.loopback = false;
+}
+
+StateMachine::StateMachine()
+{
+    reset();
+    network_debug_.ncsi.pending_restart = true;
+    std::memcpy(network_debug_.ncsi.test.ping.tx, echo_pattern,
+                sizeof(echo_pattern));
+}
+
+size_t StateMachine::poll_l2_config()
+{
+    size_t len = 0;
+    mac_addr_t mac;
+    net_config_->get_mac_addr(&mac);
+    ncsi_response_type_t response_type = ncsi_fsm_poll_l2_config(
+        &ncsi_state_, &network_debug_, &ncsi_buf_, &mac);
+
+    auto* response = reinterpret_cast<ncsi_simple_response_t*>(ncsi_buf_.data);
+
+    if (response_type == NCSI_RESPONSE_ACK)
+    {
+        /* If the response is MAC response, some extra handling needed. */
+        if ((NCSI_RESPONSE | NCSI_OEM_COMMAND) ==
+            response->hdr.control_packet_type)
+        {
+            auto* oem_response =
+                reinterpret_cast<ncsi_oem_simple_response_t*>(ncsi_buf_.data);
+            if (oem_response->oem_header.oem_cmd ==
+                NCSI_OEM_COMMAND_GET_HOST_MAC)
+            {
+                net_config_->set_mac_addr(mac);
+            }
+        }
+    }
+    else if (NCSI_RESPONSE_NONE == response_type)
+    {
+        /* Buffer is ready to be sent. */
+        len = ncsi_buf_.len;
+        ncsi_buf_.len = 0;
+    }
+    else
+    {
+        report_ncsi_error(response_type);
+    }
+
+    return len;
+}
+
+size_t StateMachine::poll_simple(ncsi_simple_poll_f poll_func)
+{
+    mac_addr_t mac;
+    net_config_->get_mac_addr(&mac);
+    const uint16_t rx_port = DEFAULT_ADDRESSES_RX_PORT;
+
+    ncsi_response_type_t response_type =
+        poll_func(&ncsi_state_, &network_debug_, &ncsi_buf_, &mac, 0, rx_port);
+
+    auto* response = reinterpret_cast<ncsi_simple_response_t*>(ncsi_buf_.data);
+
+    size_t len = 0;
+    if (response_type == NCSI_RESPONSE_NONE)
+    {
+        /* Buffer is ready to be sent, or we are done. */
+        len = ncsi_buf_.len;
+        ncsi_buf_.len = 0;
+    }
+    else if (response->hdr.control_packet_type ==
+             (NCSI_RESPONSE | NCSI_GET_LINK_STATUS))
+    {
+        auto status_response =
+            reinterpret_cast<ncsi_link_status_response_t*>(response);
+        bool new_link_up = ntohl(status_response->link_status.link_status) &
+                           NCSI_LINK_STATUS_UP;
+        if (!link_up_ || new_link_up != *link_up_)
+        {
+            CPRINTF("[NCSI link %s]\n", new_link_up ? "up" : "down");
+            link_up_ = new_link_up;
+        }
+    }
+    else if (response->hdr.control_packet_type ==
+             (NCSI_RESPONSE | NCSI_OEM_COMMAND))
+    {
+        auto* oem_response =
+            reinterpret_cast<ncsi_oem_simple_response_t*>(ncsi_buf_.data);
+        if (oem_response->oem_header.oem_cmd == NCSI_OEM_COMMAND_GET_FILTER)
+        {
+            bool new_hostless = ncsi_fsm_is_nic_hostless(&ncsi_state_);
+            if (!hostless_ || new_hostless != *hostless_)
+            {
+                CPRINTF("[NCSI nic %s]\n",
+                        new_hostless ? "hostless" : "hostfull");
+                net_config_->set_nic_hostless(new_hostless);
+                hostless_ = new_hostless;
+            }
+        }
+    }
+    else if (response_type != NCSI_RESPONSE_ACK)
+    {
+        report_ncsi_error(response_type);
+    }
+
+    return len;
+}
+
+void StateMachine::report_ncsi_error(ncsi_response_type_t response_type)
+{
+    char state_string[kStateFormatLen];
+    snprintf_state(state_string, sizeof(state_string), &ncsi_state_);
+    auto* response =
+        reinterpret_cast<const ncsi_simple_response_t*>(ncsi_buf_.data);
+    switch (response_type)
+    {
+        case NCSI_RESPONSE_UNDERSIZED:
+            if (!ncsi_buf_.len)
+            {
+                network_debug_.ncsi.rx_error.timeout_count++;
+                CPRINTF("[NCSI timeout in state %s]\n", state_string);
+            }
+            else
+            {
+                network_debug_.ncsi.rx_error.undersized_count++;
+                CPRINTF("[NCSI undersized response in state %s]\n",
+                        state_string);
+            }
+            break;
+        case NCSI_RESPONSE_NACK:
+            network_debug_.ncsi.rx_error.nack_count++;
+            CPRINTF(
+                "[NCSI nack in state %s. Response: 0x%04x Reason: 0x%04x]\n",
+                state_string, ntohs(response->response_code),
+                ntohs(response->reason_code));
+            break;
+        case NCSI_RESPONSE_UNEXPECTED_TYPE:
+            network_debug_.ncsi.rx_error.unexpected_type_count++;
+            CPRINTF("[NCSI unexpected response in state %s. Response type: "
+                    "0x%02x]\n",
+                    state_string, response->hdr.control_packet_type);
+            break;
+        case NCSI_RESPONSE_UNEXPECTED_SIZE:
+        {
+            int expected_size;
+            if ((NCSI_RESPONSE | NCSI_OEM_COMMAND) ==
+                response->hdr.control_packet_type)
+            {
+                auto* oem_response =
+                    reinterpret_cast<ncsi_oem_simple_response_t*>(
+                        ncsi_buf_.data);
+                expected_size = ncsi_oem_get_response_size(
+                    oem_response->oem_header.oem_cmd);
+            }
+            else
+            {
+                expected_size = ncsi_get_response_size(
+                    response->hdr.control_packet_type & (~NCSI_RESPONSE));
+            }
+            network_debug_.ncsi.rx_error.unexpected_size_count++;
+            CPRINTF("[NCSI unexpected response size in state %s."
+                    " Expected %d]\n",
+                    state_string, expected_size);
+        }
+        break;
+        case NCSI_RESPONSE_OEM_FORMAT_ERROR:
+            network_debug_.ncsi.rx_error.unexpected_type_count++;
+            CPRINTF("[NCSI OEM format error]\n");
+            break;
+        case NCSI_RESPONSE_UNEXPECTED_PARAMS:
+            CPRINTF("[NCSI OEM Filter MAC or TCP/IP Config Mismatch]\n");
+            break;
+        default:
+            /* NCSI_RESPONSE_ACK and NCSI_RESPONSE_NONE are not errors and need
+             * not be handled here, so this branch is just to complete the
+             * switch.
+             */
+            CPRINTF("[NCSI OK]\n");
+            break;
+    }
+}
+
+int StateMachine::receive_ncsi()
+{
+    int len;
+    do
+    {
+        // Return value of zero means timeout
+        len = sock_io_->recv(ncsi_buf_.data, sizeof(ncsi_buf_.data));
+        if (len > 0)
+        {
+            auto* hdr = reinterpret_cast<struct ether_header*>(ncsi_buf_.data);
+            if (ETHER_NCSI == ntohs(hdr->ether_type))
+            {
+                ncsi_buf_.len = len;
+                break;
+            }
+        }
+
+        ncsi_buf_.len = 0;
+    } while (len > 0);
+
+    return ncsi_buf_.len;
+}
+
+void StateMachine::run_test_fsm(size_t* tx_len)
+{
+    // Sleep and restart when test FSM finishes.
+    if (is_test_done())
+    {
+        std::this_thread::sleep_for(std::chrono::seconds(retest_delay_s_));
+        // Skip over busy wait in state machine - already waited.
+        ncsi_state_.retest_delay_count = NCSI_FSM_RESTART_DELAY_COUNT;
+    }
+    // until NCSI_STATE_TEST_END
+    *tx_len = poll_simple(ncsi_fsm_poll_test);
+}
+
+void StateMachine::run(int max_rounds)
+{
+    if (!net_config_ || !sock_io_)
+    {
+        CPRINTF("StateMachine configuration incomplete: "
+                "net_config_: <%p>, sock_io_: <%p>",
+                reinterpret_cast<void*>(net_config_),
+                reinterpret_cast<void*>(sock_io_));
+        return;
+    }
+
+    bool infinite_loop = (max_rounds <= 0);
+    while (infinite_loop || --max_rounds >= 0)
+    {
+        receive_ncsi();
+
+        size_t tx_len = 0;
+        switch (ncsi_fsm_connection_state(&ncsi_state_, &network_debug_))
+        {
+            case NCSI_CONNECTION_DOWN:
+            case NCSI_CONNECTION_LOOPBACK:
+                tx_len = poll_l2_config();
+                break;
+            case NCSI_CONNECTION_UP:
+                if (!is_test_done() || ncsi_fsm_is_nic_hostless(&ncsi_state_))
+                {
+                    run_test_fsm(&tx_len);
+                }
+                else
+                {
+                    // - Only start L3/L4 config when test is finished
+                    // (it will last until success
+                    // (i.e. NCSI_CONNECTION_UP_AND_CONFIGURED) or fail.
+                    tx_len = poll_simple(ncsi_fsm_poll_l3l4_config);
+                }
+                break;
+            case NCSI_CONNECTION_UP_AND_CONFIGURED:
+                run_test_fsm(&tx_len);
+                break;
+            case NCSI_CONNECTION_DISABLED:
+                if (network_debug_.ncsi.pending_restart)
+                {
+                    network_debug_.ncsi.enabled = true;
+                }
+                break;
+            default:
+                fail();
+        }
+
+        if (tx_len)
+        {
+            print_state(ncsi_state_);
+
+            sock_io_->write(ncsi_buf_.data, tx_len);
+        }
+    }
+}
+
+void StateMachine::clear_state()
+{
+    // This implicitly resets:
+    //   l2_config_state   to NCSI_STATE_L2_CONFIG_BEGIN
+    //   l3l4_config_state to NCSI_STATE_L3L4_CONFIG_BEGIN
+    //   test_state        to NCSI_STATE_TEST_BEGIN
+    std::memset(&ncsi_state_, 0, sizeof(ncsi_state_));
+}
+
+void StateMachine::fail()
+{
+    network_debug_.ncsi.fail_count++;
+    clear_state();
+}
+
+void StateMachine::set_sockio(net::SockIO* sock_io)
+{
+    sock_io_ = sock_io;
+}
+
+void StateMachine::set_net_config(net::ConfigBase* net_config)
+{
+    net_config_ = net_config;
+}
+
+void StateMachine::set_retest_delay(unsigned delay)
+{
+    retest_delay_s_ = delay;
+}
+
+} // namespace ncsi
diff --git a/ncsid/src/ncsi_state_machine.h b/ncsid/src/ncsi_state_machine.h
new file mode 100644
index 0000000..d55bba7
--- /dev/null
+++ b/ncsid/src/ncsi_state_machine.h
@@ -0,0 +1,112 @@
+#pragma once
+#include "ncsi_sockio.h"
+#include "net_config.h"
+#include "net_iface.h"
+#include "platforms/nemora/portable/ncsi_client.h"
+#include "platforms/nemora/portable/ncsi_fsm.h"
+#include "platforms/nemora/portable/net_types.h"
+
+#include <optional>
+
+namespace ncsi
+{
+
+typedef ncsi_response_type_t (*ncsi_simple_poll_f)(ncsi_state_t*,
+                                                   network_debug_t*,
+                                                   ncsi_buf_t*, mac_addr_t*,
+                                                   uint32_t, uint16_t);
+
+// This class encapsulates three state machines:
+//  * L2 -- performs basic NC-SI setup, reads NIC MAC addr
+//  * L3/4 -- once network is configured on the interface,
+//      sets up NC-SI filter in the NIC.
+//  * Test -- runs several basic NC-SI link tests, like
+//      ECHO Request/Reply, checks filter setup etc.
+//      Also, reads hostless/host-based flag from the NIC, see
+//      ncsi_fsm.c:is_nic_hostless() for details.
+class StateMachine
+{
+  public:
+    StateMachine();
+
+    void set_sockio(net::SockIO* sock_io);
+
+    void set_net_config(net::ConfigBase* net_config);
+
+    // NC-SI State Machine's main function.
+    // max_rounds = 0 means run forever.
+    void run(int max_rounds = 0);
+
+    // How often Test FSM re-runs, in seconds.
+    void set_retest_delay(unsigned int delay);
+
+  private:
+    // Reset the state machine
+    void reset();
+
+    // Poll L2 state machine. Each call advances it by one step.
+    // Its implementation is taken directly from EC.
+    size_t poll_l2_config();
+
+    // This function is used to poll both L3/4 and Test state machine,
+    // depending on the function passed in as an argument.
+    size_t poll_simple(ncsi_simple_poll_f poll_func);
+
+    // Helper function for printing NC-SI error to stdout.
+    void report_ncsi_error(ncsi_response_type_t response_type);
+
+    int receive_ncsi();
+
+    // Helper function for advancing the test FSM.
+    void run_test_fsm(size_t* tx_len);
+
+    // Clear the state and reset all state machines.
+    void clear_state();
+
+    // In current implementation this is the same as clear state,
+    // except that it also increments the failure counter.
+    void fail();
+
+    // Return true if the test state machine finished successfully.
+    bool is_test_done() const
+    {
+        return ncsi_state_.test_state == NCSI_STATE_TEST_END;
+    }
+
+    // Max number of times a state machine is going to retry a command.
+    static constexpr auto MAX_TRIES = 5;
+
+    // How long (in seconds) to wait before re-running NC-SI test state
+    // machine.
+    unsigned int retest_delay_s_ = 1;
+
+    // The last known state of the link on the NIC
+    std::optional<bool> link_up_;
+
+    // The last known hostless mode of the NIC
+    std::optional<bool> hostless_;
+
+    // net_config_ is used to query and set network configuration.
+    // The StateMachine does not own the pointer and it is the
+    // responsibility of the client to make sure that it outlives the
+    // StateMachine.
+    net::ConfigBase* net_config_ = nullptr;
+
+    // sock_io_ is used to read and write NC-SI packets.
+    // The StateMachine does not own the pointer. It is the responsibility
+    // of the client to make sure that sock_io_ outlives the StateMachine.
+    net::SockIO* sock_io_ = nullptr;
+
+    // Both ncsi_state_ and network_debug_ parameters represent the state of
+    // the NC-SI state machine. The names and definitions are taken directly
+    // from EC.
+    ncsi_state_t ncsi_state_;
+    network_debug_t network_debug_;
+
+    // Depending on the state ncsi_buf_ represents either the NC-SI packet
+    // received from the NIC or NC-SI packet that was (or about to be)
+    // sent to the NIC.
+    ncsi_buf_t ncsi_buf_;
+};
+
+} // namespace ncsi
diff --git a/ncsid/src/ncsid.cpp b/ncsid/src/ncsid.cpp
new file mode 100644
index 0000000..58842c1
--- /dev/null
+++ b/ncsid/src/ncsid.cpp
@@ -0,0 +1,33 @@
+#include <ncsi_sockio.h>
+#include <ncsi_state_machine.h>
+#include <net_config.h>
+
+#include <iostream>
+
+int main(int argc, char* argv[])
+{
+    if (argc != 2)
+    {
+        std::cout << "Usage: " << argv[0] << " <interface_name>" << std::endl;
+        return -1;
+    }
+
+    std::string iface_name(argv[1]);
+
+    net::PhosphorConfig net_config(iface_name);
+    net::IFace eth(iface_name);
+
+    ncsi::SockIO ncsi_sock;
+    ncsi_sock.init();
+    ncsi_sock.bind_to_iface(eth);
+    ncsi_sock.filter_vlans();
+
+    ncsi::StateMachine ncsi_fsm;
+    ncsi_fsm.set_sockio(&ncsi_sock);
+    ncsi_fsm.set_net_config(&net_config);
+
+    // If run ever returns -- it's an error.
+    ncsi_fsm.run();
+
+    return -1;
+}
diff --git a/ncsid/src/ncsid@.service.in b/ncsid/src/ncsid@.service.in
new file mode 100644
index 0000000..97e041e
--- /dev/null
+++ b/ncsid/src/ncsid@.service.in
@@ -0,0 +1,12 @@
+[Unit]
+Description=Run Ncsid daemon
+Wants=mapper-wait@-xyz-openbmc_project-network-%i.service
+After=mapper-wait@-xyz-openbmc_project-network-%i.service
+
+[Service]
+Restart=always
+ExecStart=@@BIN@ ncsid %I
+SyslogIdentifier=ncsid@%I
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ncsid/src/ncsid_lib.sh b/ncsid/src/ncsid_lib.sh
new file mode 100644
index 0000000..080afed
--- /dev/null
+++ b/ncsid/src/ncsid_lib.sh
@@ -0,0 +1,384 @@
+# Internal handler used for signalling child processes that they should
+# terminate.
+HandleTerm() {
+  GOT_TERM=1
+  if ShouldTerm && (( ${#CHILD_PIDS[@]} > 0 )); then
+    kill "${!CHILD_PIDS[@]}"
+  fi
+}
+
+# Sets up the signal handler and global variables needed to run interruptible
+# services that can be killed gracefully.
+InitTerm() {
+  declare -g -A CHILD_PIDS=()
+  declare -g GOT_TERM=0
+  declare -g SUPPRESS_TERM=0
+  trap HandleTerm TERM
+}
+
+# Used to suppress the handling of SIGTERM for critical components that should
+# not respect SIGTERM. To finish suppressing, use UnsuppressTerm()
+SuppressTerm() {
+  SUPPRESS_TERM=$((SUPPRESS_TERM + 1))
+}
+
+# Stops suppressing SIGTERM for a single invocation of SuppresssTerm()
+UnsuppressTerm() {
+  SUPPRESS_TERM=$((SUPPRESS_TERM - 1))
+}
+
+# Determines if we got a SIGTERM and should respect it
+ShouldTerm() {
+  (( GOT_TERM == 1 && SUPPRESS_TERM == 0 ))
+}
+
+# Internal, ensures that functions called in a subprocess properly initialize
+# their SIGTERM handling logic
+RunInterruptibleFunction() {
+  CHILD_PIDS=()
+  trap HandleTerm TERM
+  "$@"
+}
+
+# Runs the provided commandline in the background, and passes any received
+# SIGTERMS to the child. Can be waited on using WaitInterruptibleBg
+RunInterruptibleBg() {
+  if ShouldTerm; then
+    return 143
+  fi
+  if [ "$(type -t "$1")" = "function" ]; then
+    RunInterruptibleFunction "$@" &
+  else
+    "$@" &
+  fi
+  CHILD_PIDS["$!"]=1
+}
+
+# Runs the provided commandline to completion, and passes any received
+# SIGTERMS to the child.
+RunInterruptible() {
+  RunInterruptibleBg "$@" || return
+  local child_pid="$!"
+  wait "$child_pid" || true
+  unset CHILD_PIDS["$child_pid"]
+  wait "$child_pid"
+}
+
+# Waits until all of the RunInterruptibleBg() jobs have terminated
+WaitInterruptibleBg() {
+  local wait_on=("${!CHILD_PIDS[@]}")
+  if (( ${#wait_on[@]} > 0 )); then
+    wait "${wait_on[@]}" || true
+    CHILD_PIDS=()
+    local rc=0
+    local id
+    for id in "${wait_on[@]}"; do
+      wait "$id" || rc=$?
+    done
+    return $rc
+  fi
+}
+
+# Determines if an address could be a valid IPv4 address
+# NOTE: this doesn't sanitize invalid IPv4 addresses
+IsIPv4() {
+  local ip="$1"
+
+  [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
+}
+
+# Takes lines of text from an application on stdin and parses out a single
+# MAC address per line of input.
+ParseMACFromLine() {
+  sed -n 's,.*\(\([0-9a-fA-F]\{2\}:\)\{5\}[0-9a-fA-F]\{2\}\).*,\1,p'
+}
+
+# Looks up the MAC address of the IPv4 neighbor using ARP
+DetermineNeighbor4() {
+  local netdev="$1"
+  local ip="$2"
+
+  # Grep intentionally prevented from returning an error to preserve the error
+  # value of arping
+  RunInterruptible arping -f -c 5 -w 5 -I "$netdev" "$ip" | \
+    { grep 'reply from' || true; } | ParseMACFromLine
+}
+
+# Looks up the MAC address of the IPv6 neighbor using ICMPv6 ND
+DetermineNeighbor6() {
+  local netdev="$1"
+  local ip="$2"
+
+  RunInterruptible ndisc6 -1 -r 5 -w 1000 -q "$ip" "$netdev"
+}
+
+# Looks up the MAC address of the neighbor regardless of type
+DetermineNeighbor() {
+  local netdev="$1"
+  local ip="$2"
+
+  if IsIPv4 "$ip"; then
+    DetermineNeighbor4 "$netdev" "$ip"
+  else
+    DetermineNeighbor6 "$netdev" "$ip"
+  fi
+}
+
+# Performs a mapper call to get the subroot for the object root
+# with a maxdepth and list of required interfaces. Returns a streamed list
+# of JSON objects that contain an { object, service }.
+GetSubTree() {
+  local root="$1"
+  shift
+  local max_depth="$1"
+  shift
+
+  busctl --json=short call \
+      'xyz.openbmc_project.ObjectMapper' \
+      '/xyz/openbmc_project/object_mapper' \
+      'xyz.openbmc_project.ObjectMapper' \
+      'GetSubTree' sias "$root" "$max_depth" "$#" "$@" | \
+    jq -c '.data[0] | to_entries[] | { object: .key, service: (.value | keys[0]) }'
+}
+
+# Returns all of the properties for a DBus interface on an object as a JSON
+# object where the keys are the property names
+GetProperties() {
+  local service="$1"
+  local object="$2"
+  local interface="$3"
+
+  busctl --json=short call \
+      "$service" \
+      "$object" \
+      'org.freedesktop.DBus.Properties' \
+      'GetAll' s "$interface" | \
+    jq -c '.data[0] | with_entries({ key, value: .value.data })'
+}
+
+# Returns the property for a DBus interface on an object
+GetProperty() {
+  local service="$1"
+  local object="$2"
+  local interface="$3"
+  local property="$4"
+
+  busctl --json=short call \
+      "$service" \
+      "$object" \
+      'org.freedesktop.DBus.Properties' \
+      'Get' ss "$interface" "$property" | \
+    jq -r '.data[0].data'
+}
+
+# Deletes any OpenBMC DBus object from a service
+DeleteObject() {
+  local service="$1"
+  local object="$2"
+
+  busctl call \
+    "$service" \
+    "$object" \
+    'xyz.openbmc_project.Object.Delete' \
+    'Delete'
+}
+
+# Transforms the given JSON dictionary into bash local variable
+# statements that can be directly evaluated by the interpreter
+JSONToVars() {
+  jq -r 'to_entries[] | @sh "local \(.key)=\(.value)"'
+}
+
+# Returns the DBus object root for the ethernet interface
+EthObjRoot() {
+  local netdev="$1"
+
+  echo "/xyz/openbmc_project/network/$netdev"
+}
+
+# Returns the DBus object root for the static neighbors of an intrerface
+StaticNeighborObjRoot() {
+  local netdev="$1"
+
+  echo "$(EthObjRoot "$netdev")/static_neighbor"
+}
+
+# Returns all of the neighbor { service, object } data for an interface as if
+# a call to GetSubTree() was made
+GetNeighborObjects() {
+  local netdev="$1"
+
+  GetSubTree "$(StaticNeighborObjRoot "$netdev")" 0 \
+    'xyz.openbmc_project.Network.Neighbor'
+}
+
+# Returns the neighbor properties as a JSON object
+GetNeighbor() {
+  local service="$1"
+  local object="$2"
+
+  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.Neighbor'
+}
+
+# Adds a static neighbor to the system network daemon
+AddNeighbor() {
+  local service="$1"
+  local netdev="$2"
+  local ip="$3"
+  local mac="$4"
+
+  busctl call \
+    "$service" \
+    "$(EthObjRoot "$netdev")" \
+    'xyz.openbmc_project.Network.Neighbor.CreateStatic' \
+    'Neighbor' ss "$ip" "$mac" >/dev/null
+}
+
+# Returns all of the IP { service, object } data for an interface as if
+# a call to GetSubTree() was made
+GetIPObjects() {
+  local netdev="$1"
+
+  GetSubTree "$(EthObjRoot "$netdev")" 0 \
+    'xyz.openbmc_project.Network.IP'
+}
+
+# Returns the IP properties as a JSON object
+GetIP() {
+  local service="$1"
+  local object="$2"
+
+  GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
+}
+
+# Adds a static IP to the system network daemon
+AddIP() {
+  local service="$1"
+  local netdev="$2"
+  local ip="$3"
+  local prefix="$4"
+
+  local protocol='xyz.openbmc_project.Network.IP.Protocol.IPv4'
+  if ! IsIPv4 "$ip"; then
+    protocol='xyz.openbmc_project.Network.IP.Protocol.IPv6'
+  fi
+
+  busctl call \
+    "$service" \
+    "$(EthObjRoot "$netdev")" \
+    'xyz.openbmc_project.Network.IP.Create' \
+    'IP' ssys "$protocol" "$ip" "$prefix" '' >/dev/null
+}
+
+# Determines if two IP addresses have the same address family
+# IE: Both are IPv4 or both are IPv6
+MatchingAF() {
+  local rc1=0 rc2=0
+  IsIPv4 "$1" || rc1=$?
+  IsIPv4 "$2" || rc2=$?
+  (( rc1 == rc2 ))
+}
+
+# Checks to see if the machine has the provided IP address information
+# already configured. If not, it deletes all of the information for that
+# address family and adds the provided IP address.
+UpdateIP() {
+  local service="$1"
+  local netdev="$2"
+  local ip="$3"
+  local prefix="$4"
+
+  local should_add=1
+  local delete_services=()
+  local delete_objects=()
+  local entry
+  while read entry; do
+    eval "$(echo "$entry" | JSONToVars)" || return $?
+    eval "$(GetIP "$service" "$object" | JSONToVars)" || return $?
+    if [ "$(normalize_ip "$Address")" = "$(normalize_ip "$ip")" ] && \
+        [ "$PrefixLength" = "$prefix" ]; then
+      should_add=0
+    elif MatchingAF "$ip" "$Address"; then
+      echo "Deleting spurious IP: $Address/$PrefixLength" >&2
+      delete_services+=("$service")
+      delete_objects+=("$object")
+    fi
+  done < <(GetIPObjects "$netdev")
+
+  local i
+  for (( i=0; i<${#delete_objects[@]}; ++i )); do
+    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || return $?
+  done
+
+  if (( should_add == 0 )); then
+    echo "Not adding IP: $ip/$prefix" >&2
+  else
+    echo "Adding IP: $ip/$prefix" >&2
+    AddIP "$service" "$netdev" "$ip" "$prefix" || return $?
+  fi
+}
+
+# Sets the system gateway property to the provided IP address if not already
+# set to the current value.
+UpdateGateway() {
+  local service="$1"
+  local ip="$2"
+
+  local object='/xyz/openbmc_project/network/config'
+  local interface='xyz.openbmc_project.Network.SystemConfiguration'
+  local property='DefaultGateway'
+  if ! IsIPv4 "$ip"; then
+    property='DefaultGateway6'
+  fi
+
+  local current_ip
+  current_ip="$(GetProperty "$service" "$object" "$interface" "$property")" || \
+    return $?
+  if [ -n "$current_ip" ] && \
+      [ "$(normalize_ip "$ip")" = "$(normalize_ip "$current_ip")" ]; then
+    echo "Not reconfiguring gateway: $ip" >&2
+    return 0
+  fi
+
+  echo "Setting gateway: $ip" >&2
+  busctl set-property "$service" "$object" "$interface" "$property" s "$ip"
+}
+
+# Checks to see if the machine has the provided neighbor information
+# already configured. If not, it deletes all of the information for that
+# address family and adds the provided neighbor entry.
+UpdateNeighbor() {
+  local service="$1"
+  local netdev="$2"
+  local ip="$3"
+  local mac="$4"
+
+  local should_add=1
+  local delete_services=()
+  local delete_objects=()
+  local entry
+  while read entry; do
+    eval "$(echo "$entry" | JSONToVars)" || return $?
+    eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
+    if [ "$(normalize_ip "$IPAddress")" = "$(normalize_ip "$ip")" ] && \
+        [ "$(normalize_mac "$MACAddress")" = "$(normalize_mac "$mac")" ]; then
+      should_add=0
+    elif MatchingAF "$ip" "$IPAddress"; then
+      echo "Deleting spurious neighbor: $IPAddress $MACAddress" >&2
+      delete_services+=("$service")
+      delete_objects+=("$object")
+    fi
+  done < <(GetNeighborObjects "$netdev" 2>/dev/null)
+
+  local i
+  for (( i=0; i<${#delete_objects[@]}; ++i )); do
+    DeleteObject "${delete_services[$i]}" "${delete_objects[$i]}" || return $?
+  done
+
+  if (( should_add == 0 )); then
+    echo "Not adding neighbor: $ip $mac" >&2
+  else
+    echo "Adding neighbor: $ip $mac" >&2
+    AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
+  fi
+}
diff --git a/ncsid/src/ncsid_udhcpc4.script b/ncsid/src/ncsid_udhcpc4.script
new file mode 100644
index 0000000..f1ba616
--- /dev/null
+++ b/ncsid/src/ncsid_udhcpc4.script
@@ -0,0 +1,72 @@
+#!/bin/bash
+source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
+
+DetermineRouterMac() {
+  # Attempt to find the neighbor once, in case our configuration is already
+  # valid. Errors are silenced to make the logs more clear. The next call
+  # will print any real errors.
+  if DetermineNeighbor4 "$interface" "$router" 2>/dev/null; then
+    return 0
+  fi
+
+  # arping might not have a valid source address, so we need to assign
+  # the given address so arping has a source to write into the request
+  # packet. We don't want a persistent configuration yet so we modify
+  # the kernel directly.
+  if ! ip -4 addr flush dev "$interface"; then
+    echo "Failed to flush $interface" >&2
+    return 1
+  fi
+  if ! ip addr add "$ip/$mask" dev "$interface"; then
+    echo "Failed to assign $ip/$mask to $interface" >&2
+    # Don't return, because we need to reset networkd
+  fi
+
+  local rc=0
+  DetermineNeighbor4 "$interface" "$router" || rc=$?
+
+  # We need to ensure that our old network configuration gets
+  # restored, in case our early flushing breaks things.
+  systemctl restart systemd-networkd || return $?
+  return $rc
+}
+
+HandleDHCP4() {
+  local op="$1"
+
+  if [ "$op" = "bound" ]; then
+    echo "INTF: $interface" >&2
+    echo "IP: $ip/$mask" >&2
+    echo "GW: $router" >&2
+
+    local router_mac
+    if ! router_mac="$(DetermineRouterMac "$interface" "$router")"; then
+      echo "Failed to acquire gateway mac for $router" >&2
+      return 1
+    fi
+    echo "GW_MAC: $router_mac" >&2
+
+    SuppressTerm
+    local service='xyz.openbmc_project.Network'
+    local rc=0
+    UpdateIP "$service" "$interface" "$ip" "$mask" && \
+      UpdateGateway "$service" "$router" && \
+      UpdateNeighbor "$service" "$interface" "$router" "$router_mac" || \
+      rc=$?
+    UnsuppressTerm
+    touch /run/dhcp4.done
+    return $rc
+  fi
+}
+
+Main() {
+  set -o nounset
+  set -o errexit
+  set -o pipefail
+
+  InitTerm
+  HandleDHCP4 "$@"
+}
+
+return 0 2>/dev/null
+Main "$@"
diff --git a/ncsid/src/ncsid_udhcpc6.script b/ncsid/src/ncsid_udhcpc6.script
new file mode 100644
index 0000000..c5a3d7f
--- /dev/null
+++ b/ncsid/src/ncsid_udhcpc6.script
@@ -0,0 +1,62 @@
+#!/bin/bash
+source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
+
+DiscoverRouter6() {
+  local netdev="$1"
+
+  local output
+  local st=0
+  output="$(RunInterruptible rdisc6 -1 -r 5 -w 1000 -n "$netdev")" || st=$?
+  if (( st != 0 )); then
+    echo "rdisc6 failed with: " >&2
+    echo "$output" >&2
+    return $st
+  fi
+
+  local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
+  local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
+  printf '{"router_ip":"%s","router_mac":"%s"}\n' "$ip" "$mac"
+}
+
+HandleDHCP6() {
+  local op="$1"
+
+  if [ "$op" = "bound" ]; then
+    echo "INTF: $interface" >&2
+    echo "IP: $ipv6/128" >&2
+
+    local disc
+    if ! disc="$(DiscoverRouter6 "$interface")"; then
+      echo "Failed to discover router" >&2
+      return 1
+    fi
+    local vars
+    vars="$(echo "$disc" | JSONToVars)" || return
+    eval "$vars" || return
+    echo "GW: $router_ip" >&2
+    echo "GW_MAC: $router_mac" >&2
+
+    SuppressTerm
+    local service='xyz.openbmc_project.Network'
+    local rc=0
+    UpdateIP "$service" "$interface" "$ipv6" '128' && \
+      UpdateGateway "$service" "$router_ip" && \
+      UpdateNeighbor "$service" "$interface" "$router_ip" "$router_mac" || \
+      rc=$?
+    UnsuppressTerm
+    touch /run/dhcp6.done
+    return $rc
+  fi
+}
+
+Main() {
+  set -o nounset
+  set -o errexit
+  set -o pipefail
+
+  InitTerm
+  HandleDHCP6 "$@"
+}
+
+return 0 2>/dev/null
+Main "$@"
diff --git a/ncsid/src/net_config.cpp b/ncsid/src/net_config.cpp
new file mode 100644
index 0000000..f5ffc9b
--- /dev/null
+++ b/ncsid/src/net_config.cpp
@@ -0,0 +1,183 @@
+#include "net_config.h"
+
+#include <fmt/format.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <sdbusplus/bus.hpp>
+#include <stdplus/util/string.hpp>
+
+#include <cstdio>
+#include <cstring>
+#include <utility>
+#include <variant>
+
+/* Most of the code for interacting with DBus is from
+ * phosphor-host-ipmid/utils.cpp
+ */
+
+namespace net
+{
+
+namespace
+{
+
+constexpr auto IFACE_ROOT = "/xyz/openbmc_project/network/";
+constexpr auto MAC_FORMAT = "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx";
+// 2 chars for every byte + 5 colons + Null byte
+constexpr auto MAC_FORMAT_LENGTH = 6 * 2 + 5 + 1;
+constexpr auto MAC_INTERFACE = "xyz.openbmc_project.Network.MACAddress";
+constexpr auto NETWORK_SERVICE = "xyz.openbmc_project.Network";
+constexpr auto PROP_INTERFACE = "org.freedesktop.DBus.Properties";
+
+int parse_mac(const std::string& mac_addr, mac_addr_t* mac)
+{
+    int ret =
+        sscanf(mac_addr.c_str(), MAC_FORMAT, mac->octet, mac->octet + 1,
+               mac->octet + 2, mac->octet + 3, mac->octet + 4, mac->octet + 5);
+
+    return ret < 6 ? -1 : 0;
+}
+
+std::string format_mac(const mac_addr_t& mac)
+{
+    // 2 chars for every byte + 5 colons + Null byte
+    char mac_str[MAC_FORMAT_LENGTH];
+    snprintf(mac_str, sizeof(mac_str), MAC_FORMAT, mac.octet[0], mac.octet[1],
+             mac.octet[2], mac.octet[3], mac.octet[4], mac.octet[5]);
+
+    return std::string{mac_str};
+}
+
+} // namespace
+
+PhosphorConfig::PhosphorConfig(const std::string& iface_name) :
+    iface_name_{iface_name}, iface_path_{std::string(IFACE_ROOT) + iface_name},
+    shared_host_mac_(std::experimental::nullopt),
+    bus(sdbusplus::bus::new_default())
+{}
+
+sdbusplus::message::message
+    PhosphorConfig::new_networkd_call(sdbusplus::bus::bus* dbus, bool get) const
+{
+    auto networkd_call =
+        dbus->new_method_call(NETWORK_SERVICE, iface_path_.c_str(),
+                              PROP_INTERFACE, get ? "Get" : "Set");
+
+    networkd_call.append(MAC_INTERFACE, "MACAddress");
+
+    return networkd_call;
+}
+
+int PhosphorConfig::get_mac_addr(mac_addr_t* mac)
+{
+    if (mac == nullptr)
+    {
+        fmt::print(stderr, "mac is nullptr\n");
+        return -1;
+    }
+
+    // Cache hit: we have stored host MAC.
+    if (shared_host_mac_)
+    {
+        *mac = shared_host_mac_.value();
+    }
+    else // Cache miss: read MAC over DBus, and store in cache.
+    {
+        std::string mac_string;
+        try
+        {
+            auto networkd_call = new_networkd_call(&bus, true);
+            auto reply = bus.call(networkd_call);
+            std::variant<std::string> result;
+            reply.read(result);
+            mac_string = std::get<std::string>(result);
+        }
+        catch (const sdbusplus::exception::SdBusError& ex)
+        {
+            fmt::print(stderr, "Failed to get MACAddress: {}\n", ex.what());
+            return -1;
+        }
+
+        if (parse_mac(mac_string, mac) < 0)
+        {
+            fmt::print(stderr, "Failed to parse MAC Address `{}`\n",
+                       mac_string);
+            return -1;
+        }
+
+        shared_host_mac_ = *mac;
+    }
+
+    return 0;
+}
+
+int PhosphorConfig::set_mac_addr(const mac_addr_t& mac)
+{
+    auto networkd_call = new_networkd_call(&bus, false);
+    std::variant<std::string> mac_value(format_mac(mac));
+    networkd_call.append(mac_value);
+
+    try
+    {
+        auto reply = bus.call(networkd_call);
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        fmt::print(stderr, "Failed to set MAC Addr `{}`: {}\n",
+                   std::get<std::string>(mac_value), ex.what());
+        return -1;
+    }
+
+    shared_host_mac_ = std::experimental::nullopt;
+    return 0;
+}
+
+int PhosphorConfig::set_nic_hostless(bool is_nic_hostless)
+{
+    // Ensure that we don't trigger the target multiple times. This is
+    // undesirable because it will cause any inactive services to re-trigger
+    // every time we run this code. Since the loop calling this executes this
+    // code every 1s, we don't want to keep re-executing services. A fresh
+    // start of the daemon will always trigger the service to ensure system
+    // consistency.
+    if (was_nic_hostless_ && is_nic_hostless == *was_nic_hostless_)
+    {
+        return 0;
+    }
+
+    static constexpr auto systemdService = "org.freedesktop.systemd1";
+    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
+    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
+
+    auto method = bus.new_method_call(systemdService, systemdRoot,
+                                      systemdInterface, "StartUnit");
+    if (is_nic_hostless)
+    {
+        method.append(
+            stdplus::util::strCat("nic-hostless@", iface_name_, ".target"));
+    }
+    else
+    {
+        method.append(
+            stdplus::util::strCat("nic-hostful@", iface_name_, ".target"));
+    }
+
+    // Specify --job-mode (see systemctl(1) for detail).
+    method.append("replace");
+
+    try
+    {
+        bus.call_noreply(method);
+        was_nic_hostless_ = is_nic_hostless;
+        return 0;
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        fmt::print(stderr, "Failed to set systemd nic status: {}\n", ex.what());
+        return 1;
+    }
+}
+
+} // namespace net
diff --git a/ncsid/src/net_config.h b/ncsid/src/net_config.h
new file mode 100644
index 0000000..057fcc3
--- /dev/null
+++ b/ncsid/src/net_config.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "platforms/nemora/portable/net_types.h"
+
+#include <net_iface.h>
+
+#include <sdbusplus/bus.hpp>
+
+#include <experimental/optional>
+#include <list>
+#include <map>
+#include <optional>
+#include <string>
+#include <vector>
+
+// The API for configuring and querying network.
+
+namespace net
+{
+
+using DBusObjectPath = std::string;
+using DBusService = std::string;
+using DBusInterface = std::string;
+using ObjectTree =
+    std::map<DBusObjectPath, std::map<DBusService, std::vector<DBusInterface>>>;
+
+class ConfigBase
+{
+  public:
+    virtual ~ConfigBase() = default;
+
+    virtual int get_mac_addr(mac_addr_t* mac) = 0;
+
+    virtual int set_mac_addr(const mac_addr_t& mac) = 0;
+
+    // Called each time is_nic_hostless state is sampled.
+    virtual int set_nic_hostless(bool is_nic_hostless) = 0;
+};
+
+// Calls phosphord-networkd
+class PhosphorConfig : public ConfigBase
+{
+  public:
+    explicit PhosphorConfig(const std::string& iface_name);
+
+    // Reads the MAC address from phosphor-networkd interface or internal
+    // cache, and store in the mac pointer.
+    // Returns -1 if failed, 0 if succeeded.
+    int get_mac_addr(mac_addr_t* mac) override;
+
+    // Sets the MAC address over phosphor-networkd, and update internal
+    // cache.
+    // Returns -1 if failed, 0 if succeeded.
+    int set_mac_addr(const mac_addr_t& mac) override;
+
+    virtual int set_nic_hostless(bool is_nic_hostless) override;
+
+  private:
+    sdbusplus::message::message new_networkd_call(sdbusplus::bus::bus* dbus,
+                                                  bool get = false) const;
+
+    const std::string iface_name_;
+    const std::string iface_path_;
+
+    // Stores the currently configured nic state, if previously set
+    std::optional<bool> was_nic_hostless_;
+
+    // The MAC address obtained from NIC.
+    // ncsid will commit this MAC address over DBus to phosphor-networkd
+    // and expect it to be persisted. If actual host MAC address changes or
+    // BMC MAC address is overwritten, a daemon reboot is needed to reset
+    // the MAC.
+    //   Initialized to nullopt which evaluates to false. Once a value is
+    // set, bool() evaluates to true.
+    std::experimental::optional<mac_addr_t> shared_host_mac_;
+
+    // List of outstanding pids for config jobs
+    std::list<pid_t> running_pids_;
+
+    // Holds a reference to the bus for issuing commands to update network
+    // config
+    sdbusplus::bus::bus bus;
+};
+
+} // namespace net
diff --git a/ncsid/src/net_iface.cpp b/ncsid/src/net_iface.cpp
new file mode 100644
index 0000000..753ab74
--- /dev/null
+++ b/ncsid/src/net_iface.cpp
@@ -0,0 +1,120 @@
+#include "net_iface.h"
+
+#include <linux/if_packet.h>
+#include <net/ethernet.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <cstdio>
+#include <cstring>
+#include <stdexcept>
+
+namespace net
+{
+
+IFaceBase::IFaceBase(const std::string& name) : name_{name}
+{
+    if (name.size() >= IFNAMSIZ)
+    {
+        throw std::length_error("Interface name is too long");
+    }
+}
+
+int IFaceBase::get_index() const
+{
+    struct ifreq ifr;
+    std::memset(&ifr, 0, sizeof(ifr));
+    int ret = ioctl(SIOCGIFINDEX, &ifr);
+    if (ret < 0)
+    {
+        return ret;
+    }
+
+    return ifr.ifr_ifindex;
+}
+
+int IFaceBase::set_sock_flags(int sockfd, short flags) const
+{
+    return mod_sock_flags(sockfd, flags, true);
+}
+
+int IFaceBase::clear_sock_flags(int sockfd, short flags) const
+{
+    return mod_sock_flags(sockfd, flags, false);
+}
+
+int IFaceBase::mod_sock_flags(int sockfd, short flags, bool set) const
+{
+    struct ifreq ifr;
+    std::memset(&ifr, 0, sizeof(ifr));
+
+    int ret = ioctl_sock(sockfd, SIOCGIFFLAGS, &ifr);
+    if (ret < 0)
+    {
+        return ret;
+    }
+
+    if (set)
+    {
+        ifr.ifr_flags |= flags;
+    }
+    else
+    {
+        ifr.ifr_flags &= ~flags;
+    }
+    return ioctl_sock(sockfd, SIOCSIFFLAGS, &ifr);
+}
+
+int IFace::ioctl_sock(int sockfd, int request, struct ifreq* ifr) const
+{
+    if (ifr == nullptr)
+    {
+        return -1;
+    }
+
+    /* Avoid string truncation. */
+    size_t len = name_.length();
+    if (len + 1 >= sizeof(ifr->ifr_name))
+    {
+        return -1;
+    }
+
+    std::memcpy(ifr->ifr_name, name_.c_str(), len);
+    ifr->ifr_name[len] = 0;
+
+    return ::ioctl(sockfd, request, ifr);
+}
+
+int IFace::bind_sock(int sockfd, struct sockaddr_ll* saddr) const
+{
+    if (saddr == nullptr)
+    {
+        return -1;
+    }
+
+    saddr->sll_ifindex = get_index();
+
+    return bind(sockfd, reinterpret_cast<struct sockaddr*>(saddr),
+                sizeof(*saddr));
+}
+
+int IFace::ioctl(int request, struct ifreq* ifr) const
+{
+    if (ifr == nullptr)
+    {
+        return -1;
+    }
+
+    int tempsock = socket(AF_INET, SOCK_DGRAM, 0);
+    if (tempsock < 0)
+    {
+        return tempsock;
+    }
+
+    int ret = ioctl_sock(tempsock, request, ifr);
+    close(tempsock);
+
+    return ret;
+}
+
+} // namespace net
diff --git a/ncsid/src/net_iface.h b/ncsid/src/net_iface.h
new file mode 100644
index 0000000..ebd6edc
--- /dev/null
+++ b/ncsid/src/net_iface.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include <linux/if.h>
+#include <linux/if_packet.h>
+#include <net/ethernet.h>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+#include <functional>
+#include <string>
+
+namespace net
+{
+
+class IFaceBase
+{
+  public:
+    explicit IFaceBase(const std::string& name);
+    virtual ~IFaceBase() = default;
+
+    /** @brief Get the index of the network interface corresponding
+     * to this object.
+     */
+    int get_index() const;
+
+    /** @brief Set interface flags using provided socket.
+     *
+     *  @param[in] sockfd - Socket to use for SIOCSIFFLAGS ioctl call.
+     *  @param[in] flags - Flags to set.
+     */
+    int set_sock_flags(int sockfd, short flags) const;
+
+    /** @brief Clear interface flags using provided socket.
+     *
+     *  @param[in] sockfd - Socket to use for SIOCSIFFLAGS/SIOCGIFFLAGS
+     *      ioctl call.
+     *  @param[in] flags - Flags to clear.
+     */
+    int clear_sock_flags(int sockfd, short flags) const;
+
+    /** @brief Bind given socket to this interface. Similar to bind
+     *     syscall, except that it fills in sll_ifindex field
+     *     of struct sockaddr_ll with the index of this interface.
+     */
+    virtual int bind_sock(int sockfd, struct sockaddr_ll* saddr) const = 0;
+
+  protected:
+    std::string name_;
+
+  private:
+    /** @brief Similar to ioctl syscall, but the socket is created inside
+     *      the function and the interface name in struct ifreq is
+     *      properly populated with the index of this interface.
+     */
+    virtual int ioctl(int request, struct ifreq* ifr) const = 0;
+
+    /** @brief Similar to ioctl syscall. The interface index in
+     *      struct ifreq is
+     *      properly populated with the index of this interface.
+     */
+    virtual int ioctl_sock(int sockfd, int request,
+                           struct ifreq* ifr) const = 0;
+
+    /** @brief Modify interface flags, using the given socket for
+     *      ioctl call.
+     */
+    int mod_sock_flags(int sockfd, short flags, bool set) const;
+};
+
+class IFace : public IFaceBase
+{
+  public:
+    explicit IFace(const std::string& name) : IFaceBase(name)
+    {}
+
+    /** @brief Bind given socket to this interface. Similar to bind
+     *     syscall, except that it fills in sll_ifindex field
+     *     of struct sockaddr_ll with the index of this interface.
+     */
+    int bind_sock(int sockfd, struct sockaddr_ll* saddr) const override;
+
+  private:
+    /** @brief Similar to ioctl syscall, but the socket is created inside
+     *      the function and the interface name in struct ifreq is
+     *      properly populated with the index of this interface.
+     */
+    int ioctl(int request, struct ifreq* ifr) const override;
+    /** @brief Similar to ioctl syscall. The interface index in
+     *      struct ifreq is
+     *      properly populated with the index of this interface.
+     */
+    int ioctl_sock(int sockfd, int request, struct ifreq* ifr) const override;
+};
+
+} // namespace net
diff --git a/ncsid/src/net_sockio.cpp b/ncsid/src/net_sockio.cpp
new file mode 100644
index 0000000..a4d7ec6
--- /dev/null
+++ b/ncsid/src/net_sockio.cpp
@@ -0,0 +1,36 @@
+#include "net_sockio.h"
+
+#include <sys/socket.h>
+#include <unistd.h>
+
+namespace net
+{
+
+int SockIO::close()
+{
+    int ret = 0;
+    if (sockfd_ >= 0)
+    {
+        ret = ::close(sockfd_);
+        sockfd_ = -1;
+    }
+
+    return ret;
+}
+
+int SockIO::write(const void* buf, size_t len)
+{
+    return ::write(sockfd_, buf, len);
+}
+
+int SockIO::recv(void* buf, size_t maxlen)
+{
+    return ::recv(sockfd_, buf, maxlen, 0);
+}
+
+SockIO::~SockIO()
+{
+    SockIO::close();
+}
+
+} // namespace net
diff --git a/ncsid/src/net_sockio.h b/ncsid/src/net_sockio.h
new file mode 100644
index 0000000..9ed1abb
--- /dev/null
+++ b/ncsid/src/net_sockio.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <sys/types.h>
+
+namespace net
+{
+
+class SockIO
+{
+  public:
+    SockIO() = default;
+    explicit SockIO(int sockfd) : sockfd_{sockfd}
+    {}
+    virtual ~SockIO();
+
+    int get_sockfd() const
+    {
+        return sockfd_;
+    }
+
+    virtual int write(const void* buf, size_t len);
+
+    virtual int close();
+
+    virtual int recv(void* buf, size_t maxlen);
+
+  protected:
+    int sockfd_ = -1;
+};
+
+} // namespace net
diff --git a/ncsid/src/nic-hostful@.target b/ncsid/src/nic-hostful@.target
new file mode 100644
index 0000000..d4f69bf
--- /dev/null
+++ b/ncsid/src/nic-hostful@.target
@@ -0,0 +1,3 @@
+[Unit]
+Description=Target when nic enters mode with host
+Conflicts=nic-hostless@%i.target
diff --git a/ncsid/src/nic-hostless@.target b/ncsid/src/nic-hostless@.target
new file mode 100644
index 0000000..6ef66fb
--- /dev/null
+++ b/ncsid/src/nic-hostless@.target
@@ -0,0 +1,7 @@
+[Unit]
+Description=Target when nic enters mode without host
+Conflicts=nic-hostful@%i.target
+Wants=dhcp4@%i.service
+Wants=dhcp6@%i.service
+Wants=update-static-neighbors@%i.service
+Wants=update-static-neighbors@%i.timer
diff --git a/ncsid/src/normalize_ip.c b/ncsid/src/normalize_ip.c
new file mode 100644
index 0000000..107d29f
--- /dev/null
+++ b/ncsid/src/normalize_ip.c
@@ -0,0 +1,40 @@
+#include <arpa/inet.h>
+#include <stdio.h>
+
+int main(int argc, char* argv[])
+{
+    if (argc < 1)
+    {
+        return 1;
+    }
+    if (argc != 2)
+    {
+        fprintf(stderr, "Usage: %s <ip address>\n", argv[0]);
+        return 1;
+    }
+
+    union
+    {
+        struct in_addr in;
+        struct in6_addr in6;
+    } buf;
+    int af = AF_INET6;
+    if (inet_pton(af, argv[1], &buf) != 1)
+    {
+        af = AF_INET;
+        if (inet_pton(af, argv[1], &buf) != 1)
+        {
+            fprintf(stderr, "Invalid IP Address: %s\n", argv[1]);
+            return 2;
+        }
+    }
+
+    char str[INET6_ADDRSTRLEN];
+    if (inet_ntop(af, &buf, str, sizeof(str)) == NULL)
+    {
+        return 1;
+    }
+
+    printf("%s\n", str);
+    return 0;
+}
diff --git a/ncsid/src/normalize_mac.c b/ncsid/src/normalize_mac.c
new file mode 100644
index 0000000..f0eda37
--- /dev/null
+++ b/ncsid/src/normalize_mac.c
@@ -0,0 +1,51 @@
+#include <inttypes.h>
+#include <net/ethernet.h>
+#include <stdio.h>
+
+#define ETH_STRLEN (ETH_ALEN * 3)
+#define HEX_OUT "%02" PRIx8
+#define ETH_OUT                                                                \
+    HEX_OUT ":" HEX_OUT ":" HEX_OUT ":" HEX_OUT ":" HEX_OUT ":" HEX_OUT
+#define HEX_IN "%2" SCNx8
+#define ETH_IN HEX_IN ":" HEX_IN ":" HEX_IN ":" HEX_IN ":" HEX_IN ":" HEX_IN
+
+int to_ether_addr(const char* str, struct ether_addr* ret)
+{
+    char sentinel;
+    return sscanf(str, ETH_IN "%c", &ret->ether_addr_octet[0],
+                  &ret->ether_addr_octet[1], &ret->ether_addr_octet[2],
+                  &ret->ether_addr_octet[3], &ret->ether_addr_octet[4],
+                  &ret->ether_addr_octet[5], &sentinel) != ETH_ALEN;
+}
+
+void from_ether_addr(const struct ether_addr* addr, char* ret)
+{
+    sprintf(ret, ETH_OUT, addr->ether_addr_octet[0], addr->ether_addr_octet[1],
+            addr->ether_addr_octet[2], addr->ether_addr_octet[3],
+            addr->ether_addr_octet[4], addr->ether_addr_octet[5]);
+}
+
+int main(int argc, char* argv[])
+{
+    if (argc < 1)
+    {
+        return 1;
+    }
+    if (argc != 2)
+    {
+        fprintf(stderr, "Usage: %s <mac address>\n", argv[0]);
+        return 1;
+    }
+
+    struct ether_addr addr;
+    if (to_ether_addr(argv[1], &addr) != 0)
+    {
+        fprintf(stderr, "Invalid MAC Address: %s\n", argv[1]);
+        return 2;
+    }
+
+    char str[ETH_STRLEN];
+    from_ether_addr(&addr, str);
+    printf("%s\n", str);
+    return 0;
+}
diff --git a/ncsid/src/platforms/nemora/portable/default_addresses.h b/ncsid/src/platforms/nemora/portable/default_addresses.h
new file mode 100644
index 0000000..469264b
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/default_addresses.h
@@ -0,0 +1,7 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_DEFAULT_ADDRESSES_H_
+#define PLATFORMS_NEMORA_PORTABLE_DEFAULT_ADDRESSES_H_
+//
+// Nemora dedicated port. Filtered by NIC. See //depot/eng/ports.
+#define DEFAULT_ADDRESSES_RX_PORT 3959
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_DEFAULT_ADDRESSES_H_
diff --git a/ncsid/src/platforms/nemora/portable/ncsi.h b/ncsid/src/platforms/nemora/portable/ncsi.h
new file mode 100644
index 0000000..18c535d
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi.h
@@ -0,0 +1,420 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_NCSI_H_
+#define PLATFORMS_NEMORA_PORTABLE_NCSI_H_
+
+/*
+ * Module for interacting with NC-SI capable network cards.
+ *
+ * DMTF v1.0.0 NC-SI specification:
+ * http://www.dmtf.org/sites/default/files/standards/documents/DSP0222_1.0.0.pdf
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "platforms/nemora/portable/net_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef __packed
+#define __packed __attribute__((packed))
+#endif
+
+// Define states for our NC-SI connection to the NIC.
+// There is no mapping to the NC-SI specification for these states, but they
+// reflect the outcome of NC-SI commands used in our configuration state
+// machine.
+//
+// 'DOWN' - while in this state, periodically restart the configuration state
+//   machine until it succeeds.
+// 'LOOPBACK' - the response to the first NC-SI command of the configuration
+//   state machine was identical to the command: from this we infer we are in
+//   loopback. While in this state, periodically restart the configuration state
+//   machine.
+// 'UP' - all commands were responded successfully, but need DHCP configuration
+//   to go to the next state. While in this state, the connection is tested
+//   periodically for failures, which can bring back to 'DOWN'.
+// 'UP_AND_CONFIGURED' - NC-SI OEM commands for L3/L4 configuration (which
+//   depend on DHCP configuration) were responded successfully. While in this
+//   state, the connection and configuration are tested periodically for
+//   failures, which can bring back to 'DOWN'.
+// 'DISABLED' - reset default state. As soon as network is enabled (which
+//   noticeably means that ProdID must be disabled), the state goes to DOWN.
+// TODO: connection state has nothing to do with ncsi protocol and needs
+// to be moved to ncsi_fsm.h. The main problem with the move is that
+// ncsi_client.h defines an extern function with this return type, that is used
+// in a lot of places that have no business including ncsi_fsm.h
+typedef enum {
+  NCSI_CONNECTION_DOWN,
+  NCSI_CONNECTION_LOOPBACK,
+  NCSI_CONNECTION_UP,
+  NCSI_CONNECTION_UP_AND_CONFIGURED,
+  NCSI_CONNECTION_DISABLED,
+} ncsi_connection_state_t;
+
+typedef enum {
+  NCSI_RESPONSE_NONE,
+  NCSI_RESPONSE_ACK,
+  NCSI_RESPONSE_NACK,
+  NCSI_RESPONSE_UNDERSIZED,
+  NCSI_RESPONSE_UNEXPECTED_TYPE,
+  NCSI_RESPONSE_UNEXPECTED_SIZE,
+  NCSI_RESPONSE_OEM_FORMAT_ERROR,
+  NCSI_RESPONSE_TIMEOUT,
+  NCSI_RESPONSE_UNEXPECTED_PARAMS,
+} ncsi_response_type_t;
+
+// For NC-SI Rev 1.0.0, the management controller ID (mc_id) is 0.
+#define NCSI_MC_ID 0
+// For NC-SI Rev 1.0.0, the header revision is 0x01.
+#define NCSI_HEADER_REV 1
+#define NCSI_ETHERTYPE 0x88F8
+#define NCSI_RESPONSE 0x80
+
+// Command IDs
+enum {
+  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_GET_LINK_STATUS,
+  NCSI_SET_VLAN_FILTER,
+  NCSI_ENABLE_VLAN,
+  NCSI_DISABLE_VLAN,
+  NCSI_SET_MAC_ADDRESS,
+  // 0x0F is not a valid command
+  NCSI_ENABLE_BROADCAST_FILTER = 0x10,
+  NCSI_DISABLE_BROADCAST_FILTER,
+  NCSI_ENABLE_GLOBAL_MULTICAST_FILTER,
+  NCSI_DISABLE_GLOBAL_MULTICAST_FILTER,
+  NCSI_SET_NCSI_FLOW_CONTROL,
+  NCSI_GET_VERSION_ID,
+  NCSI_GET_CAPABILITIES,
+  NCSI_GET_PARAMETERS,
+  NCSI_GET_CONTROLLER_PACKET_STATISTICS,
+  NCSI_GET_NCSI_STATISTICS,
+  NCSI_GET_PASSTHROUGH_STATISTICS,
+  // 0x1B-0x4F are not valid commands
+  NCSI_OEM_COMMAND = 0x50,
+};
+// OEM Command IDs (subtypes of NCSI_OEM_COMMAND)
+#define NCSI_OEM_COMMAND_GET_HOST_MAC 0x00
+#define NCSI_OEM_COMMAND_SET_FILTER 0x01
+#define NCSI_OEM_COMMAND_GET_FILTER 0x02
+#define NCSI_OEM_COMMAND_ECHO 0x03
+
+#define NCSI_OEM_MANUFACTURER_ID 11129  // IANA Enterprise Number for Google.
+#define NCSI_OEM_ECHO_PATTERN_SIZE 64
+
+/*
+ * NCSI command frame with packet header as described in section 8.2.1.
+ * Prepended with an ethernet header.
+ */
+typedef struct __packed {
+  eth_hdr_t ethhdr;
+  uint8_t mc_id;
+  uint8_t header_revision;
+  uint8_t reserved_00;
+  uint8_t instance_id;          // Destinguish retried commands from new ones.
+  uint8_t control_packet_type;  // See section 8.3, and Table 17.
+  uint8_t channel_id;
+  uint16_t payload_length;  // In Bytes. Excludes header, checksum, padding.
+  uint16_t reserved_01[4];
+} ncsi_header_t;
+
+/*
+ * Simple NCSI response packet.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+} ncsi_simple_response_t;
+
+/*
+ * Simple NCSI command packet.
+ */
+typedef struct {
+  ncsi_header_t hdr;
+} ncsi_simple_command_t;
+
+/*
+ * Get Link Status Response. 8.4.24
+ */
+typedef struct __packed {
+  uint32_t link_status;
+  uint32_t other_indications;
+  uint32_t oem_link_status;
+} ncsi_link_status_t;
+
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_link_status_t link_status;
+} ncsi_link_status_response_t;
+
+#define NCSI_LINK_STATUS_UP (1 << 0)
+
+/*
+ * Set MAC Address packet. 8.4.31
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  mac_addr_t mac_addr;
+  uint8_t mac_addr_num;
+  uint8_t misc;
+} ncsi_set_mac_command_t;
+
+/*
+ * Enable Broadcast Filter packet. 8.4.33
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint32_t filter_settings;
+} ncsi_enable_broadcast_filter_command_t;
+
+#define NCSI_BROADCAST_FILTER_MASK_ARP         (1 << 0)
+#define NCSI_BROADCAST_FILTER_MASK_DHCP_CLIENT (1 << 1)
+#define NCSI_BROADCAST_FILTER_MASK_DHCP_SERVER (1 << 2)
+#define NCSI_BROADCAST_FILTER_MASK_NETBIOS     (1 << 3)
+
+/*
+ * Get Version ID Response. 8.4.44
+ */
+typedef struct __packed {
+  struct {
+    uint8_t major;
+    uint8_t minor;
+    uint8_t update;
+    uint8_t alpha1;
+    uint8_t reserved[3];
+    uint8_t alpha2;
+  } ncsi_version;
+  uint8_t firmware_name_string[12];
+  uint32_t firmware_version;
+  uint16_t pci_did;
+  uint16_t pci_vid;
+  uint16_t pci_ssid;
+  uint16_t pci_svid;
+  uint32_t manufacturer_id;
+} ncsi_version_id_t;
+
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_version_id_t version;
+} ncsi_version_id_response_t;
+
+/*
+ * Get Capabilities Response 8.4.46
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  uint32_t capabilities_flags;
+  uint32_t broadcast_packet_filter_capabilties;
+  uint32_t multicast_packet_filter_capabilties;
+  uint32_t buffering_capability;
+  uint32_t aen_control_support;
+  uint8_t vlan_filter_count;
+  uint8_t mixed_filter_count;
+  uint8_t multicast_filter_count;
+  uint8_t unicast_filter_count;
+  uint16_t reserved;
+  uint8_t vlan_mode_support;
+  uint8_t channel_count;
+} ncsi_capabilities_response_t;
+
+/*
+ * Get Parameters Response 8.4.48
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  // TODO: Note: Mellanox 1.4 FW has mac count swapped with mac flags.
+  uint8_t mac_address_count;
+  uint16_t reserved_01;
+  uint8_t mac_address_flags;
+  uint8_t vlan_tag_count;
+  uint8_t reserved_02;
+  uint16_t vlan_tag_flags;
+  uint32_t link_settings;
+  uint32_t broadcast_settings;
+  uint32_t configuration_flags;
+  uint8_t vlan_mode;
+  uint8_t flow_control_enable;
+  uint16_t reserved_03;
+  uint32_t aen_control;
+  mac_addr_t mac_address[2];
+  // TODO: Variable number of mac address filters (max 8. See 8.4.48)
+  uint16_t vlan_tags[2];
+  // TODO: Variable of vlan filters (up to 15 based on 8.4.48)
+} ncsi_parameters_response_t;
+
+/*
+ * Get Passthrough statistics response. 8.4.54
+ *
+ * The legacy data structure matches MLX implementation up to vX
+ * (Google vX), however the standard requires the first field to be
+ * 64bits and MLX fixed it in vX (Google vX).
+ *
+ */
+typedef struct __packed {
+  uint32_t tx_packets_received;  // EC -> NIC
+  uint32_t tx_packets_dropped;
+  uint32_t tx_channel_errors;
+  uint32_t tx_undersized_errors;
+  uint32_t tx_oversized_errors;
+  uint32_t rx_packets_received;  // Network -> NIC
+  uint32_t rx_packets_dropped;
+  uint32_t rx_channel_errors;
+  uint32_t rx_undersized_errors;
+  uint32_t rx_oversized_errors;
+} ncsi_passthrough_stats_legacy_t;
+
+typedef struct __packed {
+  uint32_t tx_packets_received_hi;  // EC -> NIC (higher 32bit)
+  uint32_t tx_packets_received_lo;  // EC -> NIC (lower 32bit)
+  uint32_t tx_packets_dropped;
+  uint32_t tx_channel_errors;
+  uint32_t tx_undersized_errors;
+  uint32_t tx_oversized_errors;
+  uint32_t rx_packets_received;  // Network -> NIC
+  uint32_t rx_packets_dropped;
+  uint32_t rx_channel_errors;
+  uint32_t rx_undersized_errors;
+  uint32_t rx_oversized_errors;
+} ncsi_passthrough_stats_t;
+
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_passthrough_stats_legacy_t stats;
+} ncsi_passthrough_stats_legacy_response_t;
+
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_passthrough_stats_t stats;
+} ncsi_passthrough_stats_response_t;
+
+/*
+ * OEM extension header for custom commands.
+ */
+typedef struct __packed {
+  uint32_t manufacturer_id;
+  uint8_t reserved[3];
+  uint8_t oem_cmd;
+} ncsi_oem_extension_header_t;
+
+/*
+ * Response format for simple OEM command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_oem_extension_header_t oem_header;
+} ncsi_oem_simple_response_t;
+
+/*
+ * Response format for OEM get MAC command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_oem_extension_header_t oem_header;
+  uint16_t reserved0;
+  uint8_t mac[6];
+} ncsi_host_mac_response_t;
+
+/*
+ * Format for OEM filter.
+ */
+typedef struct __packed {
+  uint16_t reserved0;
+  uint8_t mac[6];
+  // If ip is set to zero, the filter will match any IP address, including any
+  // IPv6 address.
+  uint32_t ip;  // Network order
+  uint16_t port;  // Network order
+  uint8_t reserved1;
+  uint8_t flags;
+  uint8_t regid[8];
+} ncsi_oem_filter_t;
+
+// Set flags
+#define NCSI_OEM_FILTER_FLAGS_ENABLE      (0x01)
+
+// Get flags
+#define NCSI_OEM_FILTER_FLAGS_ENABLED     (0x01)
+#define NCSI_OEM_FILTER_FLAGS_REGISTERED  (0x02)
+#define NCSI_OEM_FILTER_FLAGS_HOSTLESS    (0x04)
+
+/*
+ * Command format for simple OEM command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  ncsi_oem_extension_header_t oem_header;
+} ncsi_oem_simple_cmd_t;
+
+/*
+ * Response format for OEM get filter command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_oem_extension_header_t oem_header;
+  ncsi_oem_filter_t filter;
+} ncsi_oem_get_filter_response_t;
+
+/*
+ * Command format for OEM set filter command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  ncsi_oem_extension_header_t oem_header;
+  ncsi_oem_filter_t filter;
+} ncsi_oem_set_filter_cmd_t;
+
+/*
+ * Command format for OEM echo command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  ncsi_oem_extension_header_t oem_header;
+  uint8_t pattern[NCSI_OEM_ECHO_PATTERN_SIZE];
+} ncsi_oem_echo_cmd_t;
+
+/*
+ * Response format for OEM echo command.
+ */
+typedef struct __packed {
+  ncsi_header_t hdr;
+  uint16_t response_code;
+  uint16_t reason_code;
+  ncsi_oem_extension_header_t oem_header;
+  uint8_t pattern[NCSI_OEM_ECHO_PATTERN_SIZE];
+} ncsi_oem_echo_response_t;
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_NCSI_H_
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_client.c b/ncsid/src/platforms/nemora/portable/ncsi_client.c
new file mode 100644
index 0000000..38ab9e3
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_client.c
@@ -0,0 +1,307 @@
+/*
+ * Library of NC-SI commands compliant with version 1.0.0.
+ *
+ * This implements a subset of the commands provided in the specification.
+ *
+ * Checksums are optional and not implemented here. All NC-SI checksums are set
+ * to 0 to indicate that per 8.2.2.3.
+ */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <arpa/inet.h>
+
+#include "platforms/nemora/portable/ncsi_client.h"
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+#endif
+
+// todo - To save space these tables use the notion that no response is
+//            larger than 255 bytes. Need a BUILD_ASSERT.
+// todo - Replace 0's with actual sizes once the commands are supported
+static const uint8_t ncsi_response_size_table[] = {
+  sizeof(ncsi_simple_response_t),        // NCSI_CLEAR_INITIAL_STATE
+  sizeof(ncsi_simple_response_t),        // NCSI_SELECT_PACKAGE
+  sizeof(ncsi_simple_response_t),        // NCSI_DESELECT_PACKAGE
+  sizeof(ncsi_simple_response_t),        // NCSI_ENABLE_CHANNEL
+  sizeof(ncsi_simple_response_t),        // NCSI_DISABLE_CHANNEL
+  sizeof(ncsi_simple_response_t),        // NCSI_RESET_CHANNEL
+  sizeof(ncsi_simple_response_t),        // NCSI_ENABLE_CHANNEL_NETWORK_TX
+  sizeof(ncsi_simple_response_t),        // NCSI_DISABLE_CHANNEL_NETWORK_TX
+  sizeof(ncsi_simple_response_t),        // NCSI_AEN_ENABLE
+  sizeof(ncsi_simple_response_t),        // NCSI_SET_LINK
+  sizeof(ncsi_link_status_response_t),   // NCSI_GET_LINK_STATUS
+  sizeof(ncsi_simple_response_t),        // NCSI_SET_VLAN_FILTER
+  sizeof(ncsi_simple_response_t),        // NCSI_ENABLE_VLAN
+  sizeof(ncsi_simple_response_t),        // NCSI_DISABLE_VLAN
+  sizeof(ncsi_simple_response_t),        // NCSI_SET_MAC_ADDRESS
+  0,                                     // 0x0F is not a valid command
+  sizeof(ncsi_simple_response_t),        // NCSI_ENABLE_BROADCAST_FILTER
+  sizeof(ncsi_simple_response_t),        // NCSI_DISABLE_BROADCAST_FILTER
+  sizeof(ncsi_simple_response_t),        // NCSI_ENABLE_GLOBAL_MULTICAST_FILTER
+  sizeof(ncsi_simple_response_t),        // NCSI_DISABLE_GLOBAL_MULTICAST_FILTER
+  sizeof(ncsi_simple_response_t),        // NCSI_SET_NCSI_FLOW_CONTROL
+  sizeof(ncsi_version_id_response_t),    // NCSI_GET_VERSION_ID
+  sizeof(ncsi_capabilities_response_t),  // NCSI_GET_CAPABILITIES
+  sizeof(ncsi_parameters_response_t),    // NCSI_GET_PARAMETERS
+  0,  // NCSI_GET_CONTROLLER_PACKET_STATISTICS
+  0,  // NCSI_GET_NCSI_STATISTICS
+  sizeof(ncsi_passthrough_stats_response_t),  // NCSI_GET_PASSTHROUGH_STATISTICS
+};
+
+static const uint8_t ncsi_oem_response_size_table[] = {
+  sizeof(ncsi_host_mac_response_t),        // NCSI_OEM_COMMAND_GET_HOST_MAC
+  sizeof(ncsi_oem_simple_response_t),      // NCSI_OEM_COMMAND_SET_FILTER
+  sizeof(ncsi_oem_get_filter_response_t),  // NCSI_OEM_COMMAND_GET_FILTER
+  sizeof(ncsi_oem_echo_response_t),        // NCSI_OEM_COMMAND_ECHO
+};
+
+// TODO Should increment when we send a packet that is not a retry.
+static uint8_t current_instance_id;
+
+/*
+ * Sets _most_ of the NC-SI header fields. Caller is expected to set
+ * payload_length field if it is > 0. For many NC-SI commands it is 0.
+ */
+static void set_header_fields(ncsi_header_t* header, uint8_t ch_id,
+                              uint8_t cmd_type)
+{
+  // Destination MAC must be all 0xFF.
+  memset(header->ethhdr.dest.octet, 0xFF, sizeof(header->ethhdr.dest.octet));
+  // Source MAC can be any value.
+  memset(header->ethhdr.src.octet, 0xAB, sizeof(header->ethhdr.src.octet));
+  header->ethhdr.ethertype = htons(NCSI_ETHERTYPE);
+
+  // NC-SI Header
+  header->mc_id = NCSI_MC_ID;
+  header->header_revision = NCSI_HEADER_REV;
+  header->reserved_00 = 0;
+  header->instance_id = current_instance_id;
+  header->control_packet_type = cmd_type;
+  header->channel_id = ch_id;
+  header->payload_length = 0;  // Caller is expected to set this if != 0.
+  memset(header->reserved_01, 0, sizeof(header->reserved_01));
+}
+
+uint32_t ncsi_get_response_size(uint8_t cmd_type)
+{
+  return (cmd_type < ARRAY_SIZE(ncsi_response_size_table))
+             ? ncsi_response_size_table[cmd_type]
+             : 0;
+}
+
+uint32_t ncsi_oem_get_response_size(uint8_t oem_cmd_type)
+{
+  return (oem_cmd_type < ARRAY_SIZE(ncsi_oem_response_size_table))
+             ? ncsi_oem_response_size_table[oem_cmd_type]
+             : 0;
+}
+
+/*
+ * Clear initial state.
+ */
+uint32_t ncsi_cmd_clear_initial_state(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_CLEAR_INITIAL_STATE);
+  return sizeof(ncsi_simple_command_t);
+}
+
+/*
+ * Set MAC address filters.
+ */
+uint32_t ncsi_cmd_set_mac(uint8_t* buf, uint8_t channel_id, mac_addr_t* mac)
+{
+  ncsi_set_mac_command_t* cmd = (ncsi_set_mac_command_t*)buf;
+
+  set_header_fields((ncsi_header_t*)buf, channel_id, NCSI_SET_MAC_ADDRESS);
+  cmd->hdr.payload_length =
+      htons(sizeof(ncsi_set_mac_command_t) - sizeof(ncsi_header_t));
+  memcpy(cmd->mac_addr.octet, mac->octet, sizeof(cmd->mac_addr.octet));
+  cmd->mac_addr_num = 1;
+  // Unicast MAC address (AT=0), enabled (E=1).
+  cmd->misc = 0x01;
+  return sizeof(ncsi_set_mac_command_t);
+}
+
+uint32_t ncsi_cmd_enable_broadcast_filter(uint8_t* buf, uint8_t channel,
+                                          uint32_t filter_settings)
+{
+  ncsi_enable_broadcast_filter_command_t* cmd =
+      (ncsi_enable_broadcast_filter_command_t*)buf;
+  set_header_fields((ncsi_header_t*)buf, channel,
+                    NCSI_ENABLE_BROADCAST_FILTER);
+  cmd->hdr.payload_length = htons(
+      sizeof(ncsi_enable_broadcast_filter_command_t) - sizeof(ncsi_header_t));
+  cmd->filter_settings = htonl(filter_settings);
+  return sizeof(ncsi_enable_broadcast_filter_command_t);
+}
+
+
+uint32_t ncsi_cmd_disable_broadcast_filter(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel,
+                    NCSI_DISABLE_BROADCAST_FILTER);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_enable_channel(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_ENABLE_CHANNEL);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_get_link_status(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_GET_LINK_STATUS);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_reset_channel(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_RESET_CHANNEL);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_enable_tx(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel,
+                    NCSI_ENABLE_CHANNEL_NETWORK_TX);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_get_version(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_GET_VERSION_ID);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_get_capabilities(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_GET_CAPABILITIES);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_get_parameters(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_GET_PARAMETERS);
+  return sizeof(ncsi_simple_command_t);
+}
+
+uint32_t ncsi_cmd_get_passthrough_stats(uint8_t* buf, uint8_t channel)
+{
+  set_header_fields((ncsi_header_t*)buf, channel,
+                    NCSI_GET_PASSTHROUGH_STATISTICS);
+  return sizeof(ncsi_simple_command_t);
+}
+
+/* OEM commands */
+
+uint32_t ncsi_oem_cmd_get_host_mac(uint8_t* buf, uint8_t channel)
+{
+  ncsi_oem_simple_cmd_t* cmd = (ncsi_oem_simple_cmd_t*)buf;
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_OEM_COMMAND);
+  cmd->hdr.payload_length =
+      htons(sizeof(ncsi_oem_simple_cmd_t) - sizeof(ncsi_header_t));
+  cmd->oem_header.manufacturer_id = htonl(NCSI_OEM_MANUFACTURER_ID);
+  memset(cmd->oem_header.reserved, 0, sizeof(cmd->oem_header.reserved));
+  cmd->oem_header.oem_cmd = NCSI_OEM_COMMAND_GET_HOST_MAC;
+  return sizeof(ncsi_oem_simple_cmd_t);
+}
+
+uint32_t ncsi_oem_cmd_get_filter(uint8_t* buf, uint8_t channel)
+{
+  ncsi_oem_simple_cmd_t* cmd = (ncsi_oem_simple_cmd_t*)buf;
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_OEM_COMMAND);
+  cmd->hdr.payload_length =
+      htons(sizeof(ncsi_oem_simple_cmd_t) - sizeof(ncsi_header_t));
+  cmd->oem_header.manufacturer_id = htonl(NCSI_OEM_MANUFACTURER_ID);
+  memset(cmd->oem_header.reserved, 0, sizeof(cmd->oem_header.reserved));
+  cmd->oem_header.oem_cmd = NCSI_OEM_COMMAND_GET_FILTER;
+  return sizeof(ncsi_oem_simple_cmd_t);
+}
+
+uint32_t ncsi_oem_cmd_set_filter(uint8_t* buf, uint8_t channel, mac_addr_t* mac,
+                                 uint32_t ip, uint16_t port, uint8_t flags)
+{
+  ncsi_oem_set_filter_cmd_t* cmd = (ncsi_oem_set_filter_cmd_t*)buf;
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_OEM_COMMAND);
+  cmd->hdr.payload_length =
+      htons(sizeof(ncsi_oem_set_filter_cmd_t) - sizeof(ncsi_header_t));
+  cmd->oem_header.manufacturer_id = htonl(NCSI_OEM_MANUFACTURER_ID);
+  memset(cmd->oem_header.reserved, 0, sizeof(cmd->oem_header.reserved));
+  cmd->oem_header.oem_cmd = NCSI_OEM_COMMAND_SET_FILTER;
+
+  cmd->filter.reserved0 = 0;
+  memcpy(cmd->filter.mac, mac->octet, sizeof(cmd->filter.mac));
+  cmd->filter.ip = htonl(ip);
+  cmd->filter.port = htons(port);
+  cmd->filter.reserved1 = 0;
+  cmd->filter.flags = flags;
+  memset(cmd->filter.regid, 0, sizeof(cmd->filter.regid));  // reserved for set
+  return sizeof(ncsi_oem_set_filter_cmd_t);
+}
+
+uint32_t ncsi_oem_cmd_echo(uint8_t* buf, uint8_t channel, uint8_t pattern[64])
+{
+  ncsi_oem_echo_cmd_t* cmd = (ncsi_oem_echo_cmd_t*)buf;
+  set_header_fields((ncsi_header_t*)buf, channel, NCSI_OEM_COMMAND);
+  cmd->hdr.payload_length =
+      htons(sizeof(ncsi_oem_echo_cmd_t) - sizeof(ncsi_header_t));
+  cmd->oem_header.manufacturer_id = htonl(NCSI_OEM_MANUFACTURER_ID);
+  memset(cmd->oem_header.reserved, 0, sizeof(cmd->oem_header.reserved));
+  cmd->oem_header.oem_cmd = NCSI_OEM_COMMAND_ECHO;
+  memcpy(cmd->pattern, pattern, sizeof(cmd->pattern));
+  return sizeof(ncsi_oem_echo_cmd_t);
+}
+
+ncsi_response_type_t ncsi_validate_response(uint8_t* buf, uint32_t len,
+                                            uint8_t cmd_type, bool is_oem,
+                                            uint32_t expected_size) {
+  if (len < sizeof(ncsi_simple_response_t)) {
+    return NCSI_RESPONSE_UNDERSIZED;
+  }
+
+  const ncsi_simple_response_t* response = (ncsi_simple_response_t*)buf;
+  if (response->response_code || response->reason_code) {
+    return NCSI_RESPONSE_NACK;
+  }
+
+  const uint8_t std_cmd_type = is_oem ? NCSI_OEM_COMMAND : cmd_type;
+  if (response->hdr.control_packet_type != (std_cmd_type | NCSI_RESPONSE)) {
+    return NCSI_RESPONSE_UNEXPECTED_TYPE;
+  }
+
+  if (len < expected_size ||
+      ntohs(response->hdr.payload_length) !=
+          expected_size - sizeof(ncsi_header_t)) {
+    return NCSI_RESPONSE_UNEXPECTED_SIZE;
+  }
+
+  if (is_oem) {
+    /* Since the expected_size was checked above, we know that if this is an oem
+     * command, it is the right size. */
+    const ncsi_oem_simple_response_t* oem_response =
+        (ncsi_oem_simple_response_t*)buf;
+    if (oem_response->oem_header.manufacturer_id !=
+            htonl(NCSI_OEM_MANUFACTURER_ID) ||
+        oem_response->oem_header.oem_cmd != cmd_type) {
+      return NCSI_RESPONSE_OEM_FORMAT_ERROR;
+    }
+  }
+
+  return NCSI_RESPONSE_ACK;
+}
+
+ncsi_response_type_t ncsi_validate_std_response(uint8_t* buf, uint32_t len,
+                                                uint8_t cmd_type) {
+  const uint32_t expected_size = ncsi_get_response_size(cmd_type);
+  return ncsi_validate_response(buf, len, cmd_type, false, expected_size);
+}
+
+ncsi_response_type_t ncsi_validate_oem_response(uint8_t* buf, uint32_t len,
+                                                uint8_t cmd_type) {
+  const uint32_t expected_size = ncsi_oem_get_response_size(cmd_type);
+  return ncsi_validate_response(buf, len, cmd_type, true, expected_size);
+}
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_client.h b/ncsid/src/platforms/nemora/portable/ncsi_client.h
new file mode 100644
index 0000000..0354b30
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_client.h
@@ -0,0 +1,263 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_NCSI_CLIENT_H_
+#define PLATFORMS_NEMORA_PORTABLE_NCSI_CLIENT_H_
+
+/*
+ * Client module for interacting with NC-SI capable network cards.
+ *
+ * DMTF v1.0.0 NC-SI specification:
+ * http://www.dmtf.org/sites/default/files/standards/documents/DSP0222_1.0.0.pdf
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/net_types.h"
+
+#define CHANNEL_0_ID 0
+#define CHANNEL_1_ID 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Return the state of our connection to the NIC. Does not map to NC-SI spec.
+ * TODO remove this function from here. It is neither defined nor used
+ * in ncsi_client.c.
+ */
+ncsi_connection_state_t ncsi_connection_state(void);
+
+/*
+ * Return the expected length for the response to a given NC-SI comamnds
+ *
+ * Args:
+ *  cmd_type: id for the given commands as defined in the NC-SI spec
+ *
+ * Caveat: returns 0 for commands that have not been implemented yet or for
+ *         NCSI_OEM_COMMAND.
+ */
+uint32_t ncsi_get_response_size(uint8_t cmd_type);
+
+/*
+ * Return the expected length for the response to a given OEM NC-SI comamnds
+ *
+ * Args:
+ *  oem_cmd_type: id for the given OEM command as defined in the
+ *                ncsi_oem_extension_header_t (and not to be confused with the
+ *                id of standard commands)
+ */
+uint32_t ncsi_oem_get_response_size(uint8_t oem_cmd_type);
+
+/*
+ * The following commands write the message to the buffer provided.
+ * The length of the message (including the ethernet header and padding) is
+ * returned.
+ */
+
+/* Standard NC-SI commands */
+
+/*
+ * Construct MAC address filtering command. 8.4.15
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_set_mac_command_t) where command will
+ *       be placed.
+ *  channel: NC-SI channel to filter on (corresponds to a physical port).
+ *  mac_filter: MAC address to match against incoming traffic.
+ */
+uint32_t ncsi_cmd_set_mac(uint8_t* buf, uint8_t channel,
+                          mac_addr_t* mac_filter);
+
+/*
+ * Construct clear initial state command. 8.4.3
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t) where command will be
+ *       placed.
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_clear_initial_state(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct enable broadcast filter command. 8.4.33
+ *
+ * Args:
+ *  filter_settings: filter mask (host order).
+ */
+uint32_t ncsi_cmd_enable_broadcast_filter(uint8_t* buf, uint8_t channel,
+                                          uint32_t filter_settings);
+/*
+ * Construct disable broadcast filter command. 8.4.35
+ *
+ * Note: disable filtering == allow forwarding of broadcast traffic
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_disable_broadcast_filter(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct enable channel command. 8.4.9
+ *
+ * Required before any NC-SI passthrough traffic will go in or out of that
+ * channel.
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_enable_channel(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct reset channel command. 8.4.13
+ *
+ * Put channel into its initial state
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_reset_channel(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct enable TX command. 8.4.15
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_enable_tx(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct get link status command. 8.4.23
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_get_link_status(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct get capabilities command. 8.4.44
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_get_version(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct get capabilities command. 8.4.45
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_get_capabilities(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct get parameters command. 8.4.47
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_get_parameters(uint8_t* buf, uint8_t channel);
+
+/*
+ * Construct get pass-through statistics. 8.4.53
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_simple_command_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_cmd_get_passthrough_stats(uint8_t* buf, uint8_t channel);
+
+/* OEM commands */
+// TODO: Move OEM commands to another file.
+
+/*
+ * Get Host MAC address. Query the NIC for its MAC address(es).
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_oem_simple_cmd_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ */
+uint32_t ncsi_oem_cmd_get_host_mac(uint8_t* buf, uint8_t channel);
+
+/*
+ * Get filter used for RX traffic.
+ */
+uint32_t ncsi_oem_cmd_get_filter(uint8_t* buf, uint8_t channel);
+
+/*
+ * Set filter for RX traffic. Incoming packets that match all the fields
+ * specified here will be forwarded over the NC-SI link.
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_oem_set_filter_cmd_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ *  mac: mac address to filter on (byte array in network order)
+ *  ip: IPv4 address to filter on (little-endian)
+ *  port: TCP/UDP port number to filter on (little-endian)
+ *  flags: bitfield of options.
+ */
+uint32_t ncsi_oem_cmd_set_filter(uint8_t* buf, uint8_t channel, mac_addr_t* mac,
+                                 uint32_t ip, uint16_t port, uint8_t flags);
+
+/*
+ * Send NC-SI packet to test connectivity with NIC.
+ *
+ * Args:
+ *  buf: buffer of length >= sizeof(ncsi_oem_echo_cmd_t)
+ *  channel: NC-SI channel targeted (corresponds to a physical port).
+ *  pattern: echo payload.
+ */
+uint32_t ncsi_oem_cmd_echo(uint8_t* buf, uint8_t channel,
+                           uint8_t pattern[NCSI_OEM_ECHO_PATTERN_SIZE]);
+
+/*
+ * Validate NC-SI response in the buffer and return validation result.
+ * Exposes "expected_size" as part of interface to handle legacy NICs. Avoid
+ * using this function directly, use ncsi_validate_std_response or
+ * ncsi_validate_oem_response instead.
+ *
+ * Args:
+ *  buf: buffer containint NC-SI response.
+ *  len: size of the response in the buffer.
+ *  cmd_type: Id of the command *that was sent* to NIC.
+ *  is_eom: true if the response in the buffer is OEM response.
+ *  expected_size: expected size of the response.
+ */
+ncsi_response_type_t ncsi_validate_response(uint8_t* buf, uint32_t len,
+                                            uint8_t cmd_type, bool is_oem,
+                                            uint32_t expected_size);
+/*
+ * Validate NC-SI response in the buffer and return validation result.
+ *
+ * Args:
+ *  buf: buffer containint NC-SI response.
+ *  len: size of the response in the buffer.
+ *  cmd_type: Id of the command *that was sent* to NIC.
+ */
+ncsi_response_type_t ncsi_validate_std_response(uint8_t* buf, uint32_t len,
+                                                uint8_t cmd_type);
+
+/*
+ * Validate NC-SI OEM response in the buffer and return validation result.
+ *
+ * Args:
+ *  buf: buffer containint NC-SI response.
+ *  len: size of the response in the buffer.
+ *  cmd_type: Id of the OEM command *that was sent* to NIC.
+ */
+ncsi_response_type_t ncsi_validate_oem_response(uint8_t* buf, uint32_t len,
+                                                uint8_t cmd_type);
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_NCSI_CLIENT_H_
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_fsm.c b/ncsid/src/platforms/nemora/portable/ncsi_fsm.c
new file mode 100644
index 0000000..0bef65d
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_fsm.c
@@ -0,0 +1,651 @@
+#include <string.h>
+
+#include <arpa/inet.h>
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/ncsi_client.h"
+#include "platforms/nemora/portable/ncsi_fsm.h"
+
+#define GO_TO_STATE(variable, state) do { *variable = state; } while (0)
+#define GO_TO_NEXT_STATE(variable) do { (*variable)++; } while (0)
+
+// TODO - This state machine needs to be rewritten, now that we have a
+// better idea of the states and transitions involved.
+// The NC-SI related states of the state machine are currently organized in
+// request/response pairs. However when I added support for the second channel
+// this resulted in more hard-coded pairs which worked okay for X
+// (despite some ugliness, see ch_under_test below) but broke down for X
+// since it only supports 1 channel. For now just add a little more ugliness
+// by stepping by 1 or 3 when going from a pair to the next depending on whether
+// the second channel is supported (1) or not (3 - skip over the second channel
+// pair).
+#define GO_TO_NEXT_CHANNEL(variable, ncsi_state)\
+  do { *variable += (ncsi_state->channel_count == 1)\
+      ? 3 : 1; } while (0)
+
+static void ncsi_fsm_clear_state(ncsi_state_t* ncsi_state) {
+  // This implicitly resets:
+  //   l2_config_state   to NCSI_STATE_L2_CONFIG_BEGIN
+  //   l3l4_config_state to NCSI_STATE_L3L4_CONFIG_BEGIN
+  //   test_state        to NCSI_STATE_TEST_BEGIN
+  memset(ncsi_state, 0, sizeof(ncsi_state_t));
+}
+
+static void ncsi_fsm_fail(ncsi_state_t* ncsi_state,
+                          network_debug_t* network_debug) {
+  network_debug->ncsi.fail_count++;
+  memcpy(&network_debug->ncsi.state_that_failed, ncsi_state,
+         sizeof(network_debug->ncsi.state_that_failed));
+  ncsi_fsm_clear_state(ncsi_state);
+}
+
+ncsi_connection_state_t ncsi_fsm_connection_state(
+    const ncsi_state_t* ncsi_state, const network_debug_t* network_debug) {
+  if (!network_debug->ncsi.enabled) {
+    return NCSI_CONNECTION_DISABLED;
+  }
+  if (ncsi_state->l2_config_state != NCSI_STATE_L2_CONFIG_END) {
+    if (network_debug->ncsi.loopback) {
+      return NCSI_CONNECTION_LOOPBACK;
+    } else {
+      return NCSI_CONNECTION_DOWN;
+    }
+  }
+  if (ncsi_state->l3l4_config_state != NCSI_STATE_L3L4_CONFIG_END) {
+    return NCSI_CONNECTION_UP;
+  }
+  return NCSI_CONNECTION_UP_AND_CONFIGURED;
+}
+
+ncsi_response_type_t ncsi_fsm_poll_l2_config(ncsi_state_t* ncsi_state,
+                                             network_debug_t* network_debug,
+                                             ncsi_buf_t* ncsi_buf,
+                                             mac_addr_t* mac) {
+  ncsi_l2_config_state_t* const state_variable = &ncsi_state->l2_config_state;
+  ncsi_response_type_t ncsi_response_type = NCSI_RESPONSE_NONE;
+  uint32_t len = 0;
+
+  switch(*state_variable) {
+  case NCSI_STATE_RESTART:
+    if (++ncsi_state->restart_delay_count >= NCSI_FSM_RESTART_DELAY_COUNT) {
+      network_debug->ncsi.pending_restart = false;
+      GO_TO_NEXT_STATE(state_variable);
+      ncsi_state->restart_delay_count = 0;
+    }
+    break;
+  case NCSI_STATE_CLEAR_0: // necessary to get mac
+    len = ncsi_cmd_clear_initial_state(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_CLEAR_0_RESPONSE:
+    {
+      bool loopback = false;
+      ncsi_response_type = ncsi_validate_std_response(
+          ncsi_buf->data, ncsi_buf->len, NCSI_CLEAR_INITIAL_STATE);
+      if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+        GO_TO_NEXT_STATE(state_variable);
+      } else {
+        // If we did not receive a response but we did receive something,
+        // then maybe there is a physical loopback, so check that we received
+        // exactly what we sent
+        if (ncsi_buf->len >= sizeof(ncsi_simple_command_t)) {
+          ncsi_simple_command_t expected_loopback_data;
+          (void)ncsi_cmd_clear_initial_state((uint8_t*)&expected_loopback_data,
+                                             CHANNEL_0_ID);
+          if (0 == memcmp((uint8_t*)&expected_loopback_data,
+                          ncsi_buf->data, sizeof(expected_loopback_data))) {
+            loopback = true;
+          }
+        }
+        ncsi_fsm_fail(ncsi_state, network_debug);
+      }
+      network_debug->ncsi.loopback = loopback;
+    }
+    break;
+  case NCSI_STATE_GET_VERSION:
+    len = ncsi_cmd_get_version(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_GET_VERSION_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_GET_VERSION_ID);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      ncsi_version_id_response_t* get_version_response =
+          (ncsi_version_id_response_t*)ncsi_buf->data;
+      // TODO - Add check for this being actually X
+      network_debug->ncsi.mlx_legacy =
+          ((ntohl(get_version_response->version.firmware_version) >> 24) ==
+           0x08);
+      GO_TO_NEXT_CHANNEL(state_variable, ncsi_state);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_GET_CAPABILITIES:
+    len = ncsi_cmd_get_capabilities(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_GET_CAPABILITIES_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_GET_CAPABILITIES);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      const ncsi_capabilities_response_t* get_capabilities_response =
+        (ncsi_capabilities_response_t*) ncsi_buf->data;
+      if (1 != get_capabilities_response->channel_count &&
+          2 != get_capabilities_response->channel_count) {
+        /* TODO: Return Error
+        CPRINTF("[NCSI Unsupported channel count %d]\n",
+                get_capabilities_response->channel_count);
+          */
+        ncsi_fsm_fail(ncsi_state, network_debug);
+      } else {
+        ncsi_state->channel_count =
+          get_capabilities_response->channel_count;
+        GO_TO_NEXT_CHANNEL(state_variable, ncsi_state);
+      }
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_CLEAR_1:
+    len = ncsi_cmd_clear_initial_state(ncsi_buf->data, CHANNEL_1_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_CLEAR_1_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_CLEAR_INITIAL_STATE);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_RESET_CHANNEL_0:
+    if (network_debug->ncsi.pending_stop) {
+      len = ncsi_cmd_reset_channel(ncsi_buf->data, CHANNEL_0_ID);
+      GO_TO_NEXT_STATE(state_variable);
+    } else {
+      // skip resetting channels
+      GO_TO_STATE(state_variable, NCSI_STATE_GET_MAC);
+    }
+    break;
+  case NCSI_STATE_RESET_CHANNEL_0_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_RESET_CHANNEL);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_CHANNEL(state_variable, ncsi_state);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_RESET_CHANNEL_1:
+    len = ncsi_cmd_reset_channel(ncsi_buf->data, CHANNEL_1_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_RESET_CHANNEL_1_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_RESET_CHANNEL);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_STOPPED:
+    network_debug->ncsi.pending_stop = false;
+    // Reset the L2 config state machine through fail(). This state machine
+    // will not be executed again so long as 'enabled' is false.
+    network_debug->ncsi.enabled = false;
+    ncsi_fsm_fail(ncsi_state, network_debug);
+    break;
+    // TODO: Add check for MFG ID and firmware version before trying
+    // any OEM commands.
+  case NCSI_STATE_GET_MAC:
+    // Only get MAC from channel 0, because that's the one that identifies the
+    // host machine (for both MDB and DHCP).
+    len = ncsi_oem_cmd_get_host_mac(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_GET_MAC_RESPONSE:
+    ncsi_response_type = ncsi_validate_oem_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_OEM_COMMAND_GET_HOST_MAC);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      ncsi_host_mac_response_t* get_mac_response =
+        (ncsi_host_mac_response_t*) ncsi_buf->data;
+      memcpy(mac->octet, get_mac_response->mac, sizeof(mac_addr_t));
+      GO_TO_NEXT_STATE(state_variable);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_SET_MAC_FILTER_0:
+    len = ncsi_cmd_set_mac(ncsi_buf->data, CHANNEL_0_ID, mac);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_SET_MAC_FILTER_0_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_SET_MAC_ADDRESS);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_CHANNEL(state_variable, ncsi_state);
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_SET_MAC_FILTER_1:
+    len = ncsi_cmd_set_mac(ncsi_buf->data, CHANNEL_1_ID, mac);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_SET_MAC_FILTER_1_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_SET_MAC_ADDRESS);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_ENABLE_CHANNEL_0:
+    len = ncsi_cmd_enable_channel(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_ENABLE_CHANNEL_0_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_ENABLE_CHANNEL);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_CHANNEL(state_variable, ncsi_state);
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_ENABLE_CHANNEL_1:
+    len = ncsi_cmd_enable_channel(ncsi_buf->data, CHANNEL_1_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_ENABLE_CHANNEL_1_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_ENABLE_CHANNEL);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  // TODO: Enable broadcast filter to block ARP.
+  case NCSI_STATE_ENABLE_TX:
+    // The NIC FW transmits all passthrough TX on the lowest enabled channel,
+    // so there is no point in enabling TX on the second channel.
+    // TODO: - In the future we may add a check for link status,
+    //         in which case we may want to intelligently disable ch.0
+    //         (if down) and enable ch.1
+    len = ncsi_cmd_enable_tx(ncsi_buf->data, CHANNEL_0_ID);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_ENABLE_TX_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_ENABLE_CHANNEL_NETWORK_TX);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else{
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_L2_CONFIG_END:
+    // Done
+    break;
+  default:
+    ncsi_fsm_fail(ncsi_state, network_debug);
+    break;
+  }
+
+  ncsi_buf->len = len;
+  return ncsi_response_type;
+}
+
+static uint32_t write_ncsi_oem_config_filter(uint8_t* buffer, uint8_t channel,
+                                             network_debug_t* network_debug,
+                                             mac_addr_t* mac,
+                                             uint32_t ipv4_addr,
+                                             uint16_t rx_port) {
+  uint32_t len;
+  (void)ipv4_addr;
+  if (network_debug->ncsi.oem_filter_disable) {
+    mac_addr_t zero_mac = {.octet = {0,}};
+    len = ncsi_oem_cmd_set_filter(buffer, channel, &zero_mac, 0, 0, 0);
+
+  } else {
+    len = ncsi_oem_cmd_set_filter(buffer, channel, mac, 0, rx_port, 1);
+  }
+  return len;
+}
+
+ncsi_response_type_t ncsi_fsm_poll_l3l4_config(ncsi_state_t* ncsi_state,
+                                               network_debug_t* network_debug,
+                                               ncsi_buf_t* ncsi_buf,
+                                               mac_addr_t* mac,
+                                               uint32_t ipv4_addr,
+                                               uint16_t rx_port) {
+  uint32_t len = 0;
+  ncsi_response_type_t ncsi_response_type = NCSI_RESPONSE_NONE;
+
+  if (ncsi_state->l3l4_config_state == NCSI_STATE_L3L4_CONFIG_BEGIN) {
+    ncsi_state->l3l4_channel = 0;
+    ncsi_state->l3l4_waiting_response = false;
+    ncsi_state->l3l4_config_state = NCSI_STATE_CONFIG_FILTERS;
+  }
+
+  /* Go through every state with every channel. */
+  if (ncsi_state->l3l4_waiting_response) {
+    ncsi_response_type = ncsi_validate_oem_response(
+        ncsi_buf->data, ncsi_buf->len, ncsi_state->l3l4_command);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      /* Current channel ACK'ed, go to the next one. */
+      ncsi_state->l3l4_channel++;
+      if (ncsi_state->l3l4_channel >= ncsi_state->channel_count) {
+        /* All channels done, reset channel number and go to the next state.
+         * NOTE: This assumes that state numbers are sequential.*/
+        ncsi_state->l3l4_config_state += 1;
+        ncsi_state->l3l4_channel = 0;
+      }
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+
+    ncsi_state->l3l4_waiting_response = false;
+  } else {
+    // Send appropriate command.
+    switch(ncsi_state->l3l4_config_state) {
+      case NCSI_STATE_CONFIG_FILTERS:
+        len =
+            write_ncsi_oem_config_filter(ncsi_buf->data, ncsi_state->l3l4_channel,
+                                         network_debug, mac, ipv4_addr, rx_port);
+        ncsi_state->l3l4_command = NCSI_OEM_COMMAND_SET_FILTER;
+        ncsi_state->l3l4_waiting_response = true;
+        break;
+      default:
+        ncsi_fsm_fail(ncsi_state, network_debug);
+        break;
+    }
+  }
+
+  ncsi_buf->len = len;
+  return ncsi_response_type;
+}
+
+/*
+ * Start a sub-section of the state machine that runs health checks.
+ * This is dependent on the NC-SI configuration being completed
+ * (e.g. ncsi_channel_count must be known).
+ */
+static bool ncsi_fsm_start_test(network_debug_t* network_debug,
+                                uint8_t channel_count) {
+  if (network_debug->ncsi.test.max_tries > 0) {
+    network_debug->ncsi.test.runs++;
+    if (2 == channel_count) {
+      network_debug->ncsi.test.ch_under_test ^= 1;
+    } else {
+      network_debug->ncsi.test.ch_under_test = 0;
+    }
+    return true;
+  }
+  return false;
+}
+
+/*
+ * Allow for a limited number of retries for the NC-SI test because
+ * it can fail under heavy TCP/IP load (since NC-SI responses share
+ * the RX buffers in chip/$(CHIP)/net.c with TCP/IP incoming traffic).
+ */
+static bool ncsi_fsm_retry_test(network_debug_t* network_debug) {
+  const uint8_t max_tries = network_debug->ncsi.test.max_tries;
+  if (max_tries) {
+    uint8_t remaining_tries = max_tries - 1 - network_debug->ncsi.test.tries;
+    if (remaining_tries > 0) {
+      network_debug->ncsi.test.tries++;
+      return true;
+    }
+  }
+  network_debug->ncsi.test.tries = 0;
+  return false;
+}
+
+/*
+ * Returns true if we have executed an NC-SI Get OEM Filter command for all
+ * channels and the flags indicate that it is running in hostless mode.
+ * This means that we can DHCP/ARP if needed.
+ * Otherwise returns false.
+ *
+ * NOTE: We default to false, if we cannot complete the L2 config state
+ *   machine or the test sequence.
+ */
+bool ncsi_fsm_is_nic_hostless(const ncsi_state_t* ncsi_state) {
+  uint8_t flags = ncsi_state->flowsteering[0].flags;
+  if (ncsi_state->channel_count > 1) {
+    flags &= ncsi_state->flowsteering[1].flags;
+  }
+  return flags & NCSI_OEM_FILTER_FLAGS_HOSTLESS;
+}
+
+static void ncsi_fsm_update_passthrough_stats(
+    const ncsi_passthrough_stats_t* increment, network_debug_t* network_debug) {
+  ncsi_passthrough_stats_t* accumulated =
+      &network_debug->ncsi.pt_stats_be[network_debug->ncsi.test.ch_under_test];
+#define ACCUMULATE_PT_STATS(stat) accumulated->stat += increment->stat;
+  ACCUMULATE_PT_STATS(tx_packets_received_hi);
+  ACCUMULATE_PT_STATS(tx_packets_received_lo);
+  ACCUMULATE_PT_STATS(tx_packets_dropped);
+  ACCUMULATE_PT_STATS(tx_channel_errors);
+  ACCUMULATE_PT_STATS(tx_undersized_errors);
+  ACCUMULATE_PT_STATS(tx_oversized_errors);
+  ACCUMULATE_PT_STATS(rx_packets_received);
+  ACCUMULATE_PT_STATS(rx_packets_dropped);
+  ACCUMULATE_PT_STATS(rx_channel_errors);
+  ACCUMULATE_PT_STATS(rx_undersized_errors);
+  ACCUMULATE_PT_STATS(rx_oversized_errors);
+#undef ACCUMULATE_PT_STATS
+}
+
+static void ncsi_fsm_update_passthrough_stats_legacy(
+    const ncsi_passthrough_stats_legacy_t* read,
+    network_debug_t* network_debug) {
+  // Legacy MLX response does not include tx_packets_received_hi and also MLX
+  // counters
+  // are not reset on read (i.e. we cannot accumulate them).
+  ncsi_passthrough_stats_t* accumulated =
+      &network_debug->ncsi.pt_stats_be[network_debug->ncsi.test.ch_under_test];
+  accumulated->tx_packets_received_hi = 0;
+  accumulated->tx_packets_received_lo = read->tx_packets_received;
+#define COPY_PT_STATS(stat) accumulated->stat = read->stat;
+  COPY_PT_STATS(tx_packets_dropped);
+  COPY_PT_STATS(tx_channel_errors);
+  COPY_PT_STATS(tx_undersized_errors);
+  COPY_PT_STATS(tx_oversized_errors);
+  COPY_PT_STATS(rx_packets_received);
+  COPY_PT_STATS(rx_packets_dropped);
+  COPY_PT_STATS(rx_channel_errors);
+  COPY_PT_STATS(rx_undersized_errors);
+  COPY_PT_STATS(rx_oversized_errors);
+#undef COPY_PT_STATS
+}
+
+ncsi_response_type_t ncsi_fsm_poll_test(ncsi_state_t* ncsi_state,
+                                        network_debug_t* network_debug,
+                                        ncsi_buf_t* ncsi_buf, mac_addr_t* mac,
+                                        uint32_t ipv4_addr, uint16_t rx_port) {
+  ncsi_test_state_t* const state_variable =
+      &ncsi_state->test_state;
+  ncsi_response_type_t ncsi_response_type = NCSI_RESPONSE_NONE;
+  uint32_t len = 0;
+
+  switch(*state_variable) {
+  case NCSI_STATE_TEST_PARAMS:
+    if (ncsi_fsm_start_test(network_debug, ncsi_state->channel_count)) {
+      GO_TO_NEXT_STATE(state_variable);
+    } else {
+      // debugging only - skip test by setting max_tries to 0
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_END);
+    }
+    break;
+  case NCSI_STATE_ECHO:
+    len = ncsi_oem_cmd_echo(ncsi_buf->data,
+                            network_debug->ncsi.test.ch_under_test,
+                            network_debug->ncsi.test.ping.tx);
+    network_debug->ncsi.test.ping.tx_count++;
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_ECHO_RESPONSE:
+    ncsi_response_type = ncsi_validate_oem_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_OEM_COMMAND_ECHO);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      network_debug->ncsi.test.ping.rx_count++;
+      ncsi_oem_echo_response_t* echo_response =
+        (ncsi_oem_echo_response_t*) ncsi_buf->data;
+      if (0 == memcmp(echo_response->pattern,
+                      network_debug->ncsi.test.ping.tx,
+                      sizeof(echo_response->pattern))) {
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      } else {
+        network_debug->ncsi.test.ping.bad_rx_count++;
+        memcpy(network_debug->ncsi.test.ping.last_bad_rx,
+               echo_response->pattern,
+               sizeof(network_debug->ncsi.test.ping.last_bad_rx));
+      }
+    }
+    if (ncsi_fsm_retry_test(network_debug)) {
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_BEGIN);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_CHECK_FILTERS:
+    len = ncsi_oem_cmd_get_filter(ncsi_buf->data,
+                                  network_debug->ncsi.test.ch_under_test);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_CHECK_FILTERS_RESPONSE:
+    ncsi_response_type = ncsi_validate_oem_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_OEM_COMMAND_GET_FILTER);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      ncsi_oem_get_filter_response_t* get_filter_response =
+        (ncsi_oem_get_filter_response_t*) ncsi_buf->data;
+      // Stash away response because it contains information about NIC mode
+      memcpy((void*)ncsi_state->flowsteering[
+          network_debug->ncsi.test.ch_under_test].regid,
+             (void*)get_filter_response->filter.regid,
+             sizeof(ncsi_state->flowsteering[0].regid));
+      ncsi_state->flowsteering[
+          network_debug->ncsi.test.ch_under_test].flags =
+          get_filter_response->filter.flags;
+      // Test filter parameters only if we know that we configured the NIC,
+      // and if the NIC is in host-based mode (it appears to return all zeros's
+      // in hostless mode!).
+      if (NCSI_STATE_L3L4_CONFIG_END != ncsi_state->l3l4_config_state ||
+          ncsi_fsm_is_nic_hostless(ncsi_state)) {
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      }
+      ncsi_oem_set_filter_cmd_t expected;
+      (void)write_ncsi_oem_config_filter(
+          (uint8_t*)&expected, network_debug->ncsi.test.ch_under_test,
+          network_debug, mac, ipv4_addr, rx_port);
+      /* TODO: handle these responses in error reporting routine */
+      if (0 != memcmp((void*)&get_filter_response->filter.mac,
+                       (void*)&expected.filter.mac,
+                       sizeof(expected.filter.mac))) {
+        ncsi_response_type = NCSI_RESPONSE_UNEXPECTED_PARAMS;
+      } else if (get_filter_response->filter.ip != expected.filter.ip ||
+            get_filter_response->filter.port != expected.filter.port) {
+        ncsi_response_type = NCSI_RESPONSE_UNEXPECTED_PARAMS;
+      } else {
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      }
+    }
+    if (ncsi_fsm_retry_test(network_debug)) {
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_BEGIN);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_GET_PT_STATS:
+    len = ncsi_cmd_get_passthrough_stats(
+        ncsi_buf->data, network_debug->ncsi.test.ch_under_test);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_GET_PT_STATS_RESPONSE:
+    if (!network_debug->ncsi.mlx_legacy) {
+      ncsi_response_type = ncsi_validate_std_response(
+          ncsi_buf->data, ncsi_buf->len, NCSI_GET_PASSTHROUGH_STATISTICS);
+      if (ncsi_response_type == NCSI_RESPONSE_ACK) {
+        const ncsi_passthrough_stats_response_t* response =
+            (const ncsi_passthrough_stats_response_t*)ncsi_buf->data;
+        ncsi_fsm_update_passthrough_stats(&response->stats, network_debug);
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      }
+    } else {
+      uint32_t response_size =
+          ncsi_get_response_size(NCSI_GET_PASSTHROUGH_STATISTICS) -
+          sizeof(uint32_t);
+      ncsi_response_type = ncsi_validate_response(
+          ncsi_buf->data, ncsi_buf->len, NCSI_GET_PASSTHROUGH_STATISTICS, false,
+          response_size);
+      if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+        const ncsi_passthrough_stats_legacy_response_t* legacy_response =
+            (const ncsi_passthrough_stats_legacy_response_t*)ncsi_buf->data;
+        ncsi_fsm_update_passthrough_stats_legacy(&legacy_response->stats,
+                                                 network_debug);
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      }
+    }
+    if (ncsi_fsm_retry_test(network_debug)) {
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_BEGIN);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_GET_LINK_STATUS:
+    // We only care about ch.0 link status because that's the only one we use
+    // to transmit.
+    len = ncsi_cmd_get_link_status(ncsi_buf->data, 0);
+    GO_TO_NEXT_STATE(state_variable);
+    break;
+  case NCSI_STATE_GET_LINK_STATUS_RESPONSE:
+    ncsi_response_type = ncsi_validate_std_response(
+        ncsi_buf->data, ncsi_buf->len, NCSI_GET_LINK_STATUS);
+    if (NCSI_RESPONSE_ACK == ncsi_response_type) {
+      const ncsi_link_status_response_t* response =
+          (const ncsi_link_status_response_t*)ncsi_buf->data;
+      const uint32_t link_status = ntohl(response->link_status.link_status);
+      if (link_status & NCSI_LINK_STATUS_UP) {
+        GO_TO_NEXT_STATE(state_variable);
+        break;
+      }
+      // TODO: report this error.
+      // CPRINTF("[NCSI Link Status down 0x%08lx]\n", link_status);
+    }
+    if (ncsi_fsm_retry_test(network_debug)) {
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_BEGIN);
+    } else {
+      ncsi_fsm_fail(ncsi_state, network_debug);
+    }
+    break;
+  case NCSI_STATE_TEST_END:
+    network_debug->ncsi.test.tries = 0;
+    if (network_debug->ncsi.pending_restart) {
+      ncsi_fsm_fail(ncsi_state, network_debug); // (Ab)use fail to restart.
+    }
+    if (++ncsi_state->retest_delay_count >= NCSI_FSM_RETEST_DELAY_COUNT) {
+      GO_TO_STATE(state_variable, NCSI_STATE_TEST_BEGIN);
+      ncsi_state->retest_delay_count = 0;
+    }
+    break;
+  default:
+    ncsi_fsm_fail(ncsi_state, network_debug);
+    break;
+  }
+
+  ncsi_buf->len = len;
+  return ncsi_response_type;
+}
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_fsm.h b/ncsid/src/platforms/nemora/portable/ncsi_fsm.h
new file mode 100644
index 0000000..9ab2d82
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_fsm.h
@@ -0,0 +1,219 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_NCSI_FSM_H_
+#define PLATFORMS_NEMORA_PORTABLE_NCSI_FSM_H_
+
+/* Nemora NC-SI (Finite) State Machine implementation */
+
+#include <stdint.h>
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/net_types.h"
+
+/* TODO put this into config somewhere? */
+#define NCSI_FSM_RESTART_DELAY_COUNT 100
+#define NCSI_FSM_RETEST_DELAY_COUNT 100
+
+/* The network state is defined as a combination of the NC-SI connection state
+ * and the network configuration. However the two cannot be decoupled:
+ * - we cannot DHCP unless the NC-SI connection is up
+ * - we cannot do the OEM L3/L4 NC-SI configuration unless we have a valid
+ *   network configuration
+ *
+ * For additional complexity we cannot get DHCP/ARP responses after the host
+ * has loaded the Mellanox NIC driver but we want to be able to periodically
+ * test the NC-SI connection regardless of whether we have network configuration
+ * (so that flaky cables can be troubleshooted using the host interface).
+ *
+ * For this reason there are actually 3 NC-SI finite state machines:
+ * - L2 configuration (i.e. enabling all available NC-SI channel for passthrough
+ *   RX and TX, although only TX will work after the host loads the NIC driver)
+ * - L3/L4 configuration (i.e. configuring flow steering for RX traffic that
+ *   matches our IP address and dedicated Nemora port so that we can receive
+ *   Nemora requests even after the host loaded the NIC driver)
+ * - Connection test (i.e. periodically doing a ping test between the EC and the
+ *   NIC) and also ensuring that L3/L4 configuration parameters have not been
+ *   wiped out)
+ *
+ * For good karma, try to keep the state machines as linear as possible (one
+ * step after the other).
+ */
+
+typedef enum {
+  // First
+  NCSI_STATE_L2_CONFIG_BEGIN,
+  // Actual sequence
+  NCSI_STATE_RESTART = NCSI_STATE_L2_CONFIG_BEGIN,
+  NCSI_STATE_CLEAR_0,
+  NCSI_STATE_CLEAR_0_RESPONSE,
+  NCSI_STATE_GET_VERSION,
+  NCSI_STATE_GET_VERSION_RESPONSE,
+  NCSI_STATE_GET_CAPABILITIES,
+  NCSI_STATE_GET_CAPABILITIES_RESPONSE,
+  NCSI_STATE_CLEAR_1,
+  NCSI_STATE_CLEAR_1_RESPONSE,
+  NCSI_STATE_RESET_CHANNEL_0,
+  NCSI_STATE_RESET_CHANNEL_0_RESPONSE,
+  NCSI_STATE_RESET_CHANNEL_1,
+  NCSI_STATE_RESET_CHANNEL_1_RESPONSE,
+  NCSI_STATE_STOPPED,
+  NCSI_STATE_GET_MAC,
+  NCSI_STATE_GET_MAC_RESPONSE,
+  NCSI_STATE_SET_MAC_FILTER_0,
+  NCSI_STATE_SET_MAC_FILTER_0_RESPONSE,
+  NCSI_STATE_SET_MAC_FILTER_1,
+  NCSI_STATE_SET_MAC_FILTER_1_RESPONSE,
+  NCSI_STATE_ENABLE_CHANNEL_0,
+  NCSI_STATE_ENABLE_CHANNEL_0_RESPONSE,
+  NCSI_STATE_ENABLE_CHANNEL_1,
+  NCSI_STATE_ENABLE_CHANNEL_1_RESPONSE,
+  NCSI_STATE_ENABLE_TX,
+  NCSI_STATE_ENABLE_TX_RESPONSE,
+  // Last
+  NCSI_STATE_L2_CONFIG_END
+} ncsi_l2_config_state_t;
+
+typedef enum {
+  // First
+  NCSI_STATE_L3L4_CONFIG_BEGIN,
+  // Actual sequence
+  NCSI_STATE_CONFIG_FILTERS,
+  // Last
+  NCSI_STATE_L3L4_CONFIG_END
+} ncsi_l3l4_config_state_t;
+
+typedef enum {
+  // First
+  NCSI_STATE_TEST_BEGIN,
+  // Actual sequence
+  NCSI_STATE_TEST_PARAMS = NCSI_STATE_TEST_BEGIN,
+  NCSI_STATE_ECHO,
+  NCSI_STATE_ECHO_RESPONSE,
+  NCSI_STATE_CHECK_FILTERS,
+  NCSI_STATE_CHECK_FILTERS_RESPONSE,
+  NCSI_STATE_GET_PT_STATS,
+  NCSI_STATE_GET_PT_STATS_RESPONSE,
+  NCSI_STATE_GET_LINK_STATUS,
+  NCSI_STATE_GET_LINK_STATUS_RESPONSE,
+  // Last
+  NCSI_STATE_TEST_END
+} ncsi_test_state_t;
+
+typedef struct {
+  ncsi_l2_config_state_t l2_config_state;
+  ncsi_l3l4_config_state_t l3l4_config_state;
+  ncsi_test_state_t test_state;
+  // Last (OEM) command that was sent. (L3L4 SM only)
+  // Valid only if l3l4_waiting_response is true.
+  uint8_t l3l4_command;
+  // Number of the channel we are currently operating on. (L3L4 SM only)
+  uint8_t l3l4_channel;
+  // If true, means the request was sent and we are waiting for response.
+  bool l3l4_waiting_response;
+  uint8_t channel_count;
+  // The re-start and re-test delays ensures that we can flush the DMA
+  // buffers of potential out-of-sequence NC-SI packets (e.g. from
+  // packet that may have been received shortly after we timed out on
+  // them). The re-test delays also reduce the effect of NC-SI
+  // testing on more useful traffic.
+  uint8_t restart_delay_count;
+  uint8_t retest_delay_count;
+  struct {
+    uint8_t flags;
+    uint8_t regid[8];
+  } flowsteering[2];
+} ncsi_state_t;
+
+// Debug variables.
+// TODO - Change name to something more meaningful since the NC-SI test
+//   is not a debug-only feature.
+typedef struct {
+  uint32_t task_count;
+  uint32_t host_ctrl_flags;
+  struct {
+    bool enabled;
+    bool pending_stop;
+    bool pending_restart;
+    bool oem_filter_disable;
+    bool loopback;
+    bool mlx_legacy;
+    uint32_t fail_count;
+    ncsi_state_t state_that_failed;
+    uint32_t tx_count;
+    uint32_t rx_count;
+    uint32_t tx_error_count;
+    struct {
+      uint32_t timeout_count;
+      uint32_t oversized_count;
+      uint32_t undersized_count;
+      uint32_t nack_count;
+      uint32_t unexpected_size_count;
+      uint32_t unexpected_type_count;
+    } rx_error;
+    struct {
+      uint32_t runs;
+      uint8_t ch_under_test;
+      uint8_t tries;
+      uint8_t max_tries;  // 0 = skip test, 1 = restart on failure, > 1 = retry
+      struct {
+        uint8_t tx[NCSI_OEM_ECHO_PATTERN_SIZE];
+        uint32_t tx_count;
+        uint32_t rx_count;
+        uint32_t bad_rx_count;
+        uint8_t last_bad_rx[NCSI_OEM_ECHO_PATTERN_SIZE];
+      } ping;
+    } test;
+    ncsi_passthrough_stats_t pt_stats_be[2];  // big-endian as received from NIC
+  } ncsi;
+} network_debug_t;
+
+typedef struct {
+  uint8_t data[ETH_BUFFER_SIZE];
+  uint32_t len;  // Non-zero when there's a new NC-SI response.
+} ncsi_buf_t;
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ncsi_response_type_t ncsi_fsm_poll_l2_config(ncsi_state_t* ncsi_state,
+                                             network_debug_t* network_debug,
+                                             ncsi_buf_t* ncsi_buf,
+                                             mac_addr_t* mac);
+
+ncsi_response_type_t ncsi_fsm_poll_l3l4_config(ncsi_state_t* ncsi_state,
+                                               network_debug_t* network_debug,
+                                               ncsi_buf_t* ncsi_buf,
+                                               mac_addr_t* mac,
+                                               uint32_t ipv4_addr,
+                                               uint16_t rx_port);
+
+ncsi_response_type_t ncsi_fsm_poll_test(ncsi_state_t* ncsi_state,
+                                        network_debug_t* network_debug,
+                                        ncsi_buf_t* ncsi_buf, mac_addr_t* mac,
+                                        uint32_t ipv4_addr, uint16_t rx_port);
+
+/*
+ * Report a global state of the NC-SI connection as a function of the state
+ * of the 3 finite state machines.
+ * Note: Additionally for the case where the connection is down it reports
+ *   whether a loopback is inferred.
+ */
+ncsi_connection_state_t ncsi_fsm_connection_state(
+    const ncsi_state_t* ncsi_state, const network_debug_t* network_debug);
+
+/*
+ * Returns true if we have executed an NC-SI Get OEM Filter command for all
+ * channels and the flags indicate that it is running in hostless mode.
+ * This means that we can DHCP/ARP if needed.
+ * Otherwise returns false.
+ *
+ * NOTE: We default to false, if we cannot complete the L2 config state
+ *   machine or the test sequence.
+ */
+bool ncsi_fsm_is_nic_hostless(const ncsi_state_t* ncsi_state);
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_NCSI_FSM_H_
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_server.c b/ncsid/src/platforms/nemora/portable/ncsi_server.c
new file mode 100644
index 0000000..c7bc5e7
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_server.c
@@ -0,0 +1,160 @@
+/*
+ * Library of NC-SI commands compliant with version 1.0.0.
+ *
+ * This implements a subset of the commands provided in the specification.
+ *
+ * Checksums are optional and not implemented here. All NC-SI checksums are set
+ * to 0 to indicate that per 8.2.2.3.
+ */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <netinet/in.h>
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/ncsi_server.h"
+
+
+void ncsi_build_response_header(const uint8_t* request_buf,
+                                uint8_t* response_buf, uint16_t response_code,
+                                uint16_t reason_code, uint16_t payload_length) {
+  /* Copy the header from the command */
+  memcpy(response_buf, request_buf, sizeof(ncsi_header_t));
+  ncsi_simple_response_t* response = (ncsi_simple_response_t*)response_buf;
+  response->response_code = response_code;
+  response->reason_code = reason_code;
+
+  const ncsi_header_t* request_header = (const ncsi_header_t*)request_buf;
+  response->hdr.control_packet_type =
+      request_header->control_packet_type | NCSI_RESPONSE;
+  response->hdr.payload_length = htons(payload_length);
+}
+
+uint32_t ncsi_build_simple_ack(const uint8_t* request_buf,
+                               uint8_t* response_buf) {
+  /* Copy the header from the command */
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      sizeof(ncsi_simple_response_t) - sizeof(ncsi_header_t));
+
+  return sizeof(ncsi_simple_response_t);
+}
+
+uint32_t ncsi_build_simple_nack(const uint8_t* request_buf,
+                                uint8_t* response_buf, uint16_t response_code,
+                                uint16_t reason_code) {
+  ncsi_build_response_header(
+      request_buf, response_buf, response_code, reason_code,
+      sizeof(ncsi_simple_response_t) - sizeof(ncsi_header_t));
+
+  return sizeof(ncsi_simple_response_t);
+}
+
+static void ncsi_build_oem_ack(const uint8_t* request_buf,
+                               uint8_t* response_buf, uint32_t response_size) {
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      response_size - sizeof(ncsi_header_t));
+  const ncsi_oem_simple_cmd_t* oem_command =
+      (const ncsi_oem_simple_cmd_t*)request_buf;
+  ncsi_oem_simple_response_t* oem_response =
+      (ncsi_oem_simple_response_t*)response_buf;
+  memmove(&oem_response->oem_header, &oem_command->oem_header,
+          sizeof(ncsi_oem_extension_header_t));
+  oem_response->oem_header.manufacturer_id = htonl(NCSI_OEM_MANUFACTURER_ID);
+}
+
+uint32_t ncsi_build_version_id_ack(const uint8_t* request_buf,
+                                   uint8_t* response_buf,
+                                   const ncsi_version_id_t* version_id) {
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      sizeof(ncsi_version_id_response_t) - sizeof(ncsi_header_t));
+  ncsi_version_id_response_t* version_id_response =
+      (ncsi_version_id_response_t*)response_buf;
+  memcpy(&version_id_response->version, version_id, sizeof(ncsi_version_id_t));
+  return sizeof(ncsi_version_id_response_t);
+}
+
+uint32_t ncsi_build_oem_get_mac_ack(const uint8_t* request_buf,
+                                    uint8_t* response_buf,
+                                    const mac_addr_t* mac) {
+  ncsi_build_oem_ack(request_buf, response_buf,
+                     sizeof(ncsi_host_mac_response_t));
+  ncsi_host_mac_response_t* response = (ncsi_host_mac_response_t*)response_buf;
+  memcpy(response->mac, mac->octet, MAC_ADDR_SIZE);
+  return sizeof(ncsi_host_mac_response_t);
+}
+
+uint32_t ncsi_build_oem_simple_ack(const uint8_t* request_buf,
+                                   uint8_t* response_buf) {
+  ncsi_build_oem_ack(request_buf, response_buf,
+                     sizeof(ncsi_oem_simple_response_t));
+  return sizeof(ncsi_oem_simple_response_t);
+}
+
+uint32_t ncsi_build_oem_echo_ack(const uint8_t* request_buf,
+                                 uint8_t* response_buf) {
+  ncsi_oem_echo_response_t* echo_response =
+      (ncsi_oem_echo_response_t*)response_buf;
+  const ncsi_oem_echo_cmd_t* echo_cmd = (const ncsi_oem_echo_cmd_t*)request_buf;
+  memmove(echo_response->pattern, echo_cmd->pattern, sizeof(echo_cmd->pattern));
+  // Because we allow request and response to be the same buffer, it is
+  // important that pattern copy precedes the call to ncsi_build_oem_ack.
+  ncsi_build_oem_ack(request_buf, response_buf,
+                     sizeof(ncsi_oem_echo_response_t));
+
+  return sizeof(ncsi_oem_echo_response_t);
+}
+
+uint32_t ncsi_build_oem_get_filter_ack(const uint8_t* request_buf,
+                                       uint8_t* response_buf,
+                                       const ncsi_oem_filter_t* filter) {
+  ncsi_build_oem_ack(request_buf, response_buf,
+                     sizeof(ncsi_oem_get_filter_response_t));
+  ncsi_oem_get_filter_response_t* get_filter_response =
+      (ncsi_oem_get_filter_response_t*)response_buf;
+  memcpy(&get_filter_response->filter, filter,
+         sizeof(get_filter_response->filter));
+  return sizeof(ncsi_oem_get_filter_response_t);
+}
+
+uint32_t ncsi_build_pt_stats_ack(const uint8_t* request_buf,
+                                 uint8_t* response_buf,
+                                 const ncsi_passthrough_stats_t* stats) {
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      sizeof(ncsi_passthrough_stats_response_t) - sizeof(ncsi_header_t));
+  ncsi_passthrough_stats_response_t* pt_stats_response =
+      (ncsi_passthrough_stats_response_t*)response_buf;
+  /* TODO: endianness? */
+  memcpy(&pt_stats_response->stats, stats, sizeof(pt_stats_response->stats));
+  return sizeof(ncsi_passthrough_stats_response_t);
+}
+
+uint32_t ncsi_build_pt_stats_legacy_ack(
+    const uint8_t* request_buf, uint8_t* response_buf,
+    const ncsi_passthrough_stats_legacy_t* stats) {
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      sizeof(ncsi_passthrough_stats_legacy_response_t) - sizeof(ncsi_header_t));
+  ncsi_passthrough_stats_legacy_response_t* pt_stats_response =
+      (ncsi_passthrough_stats_legacy_response_t*)response_buf;
+  /* TODO: endianness? */
+  memcpy(&pt_stats_response->stats, stats, sizeof(pt_stats_response->stats));
+  return sizeof(ncsi_passthrough_stats_legacy_response_t);
+}
+
+uint32_t ncsi_build_link_status_ack(const uint8_t* request_buf,
+                                    uint8_t* response_buf,
+                                    const ncsi_link_status_t* link_status) {
+  ncsi_build_response_header(
+      request_buf, response_buf, 0, 0,
+      sizeof(ncsi_link_status_response_t) - sizeof(ncsi_header_t));
+  ncsi_link_status_response_t* link_status_response =
+      (ncsi_link_status_response_t*)response_buf;
+  memcpy(&link_status_response->link_status, link_status,
+         sizeof(link_status_response->link_status));
+  return sizeof(ncsi_link_status_response_t);
+}
diff --git a/ncsid/src/platforms/nemora/portable/ncsi_server.h b/ncsid/src/platforms/nemora/portable/ncsi_server.h
new file mode 100644
index 0000000..6a411aa
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/ncsi_server.h
@@ -0,0 +1,189 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_NCSI_SERVER_H_
+#define PLATFORMS_NEMORA_PORTABLE_NCSI_SERVER_H_
+
+/*
+ * Module for constructing NC-SI response commands on the NIC
+ *
+ * DMTF v1.0.0 NC-SI specification:
+ * http://www.dmtf.org/sites/default/files/standards/documents/DSP0222_1.0.0.pdf
+ */
+
+#include <stdint.h>
+
+#include "platforms/nemora/portable/ncsi.h"
+#include "platforms/nemora/portable/net_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Build the header for the response to the command in the buffer.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  response_code: Response Code. Must be zero for ACK.
+ *  reason_code: Reason Code. Must be zero for ACK.
+ *  payload_length: size of a payload.
+ */
+void ncsi_build_response_header(const uint8_t* request_buf,
+                                uint8_t* response_buf, uint16_t response_code,
+                                uint16_t reason_code, uint16_t payload_length);
+
+/*
+ * Construct simple ACK command in the buffer, given the buffer with the
+ * received command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_simple_ack(const uint8_t* request_buf,
+                               uint8_t* response_buf);
+
+/*
+ * Construct simple NACK command in the buffer, given the buffer with the
+ * received command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  response_code: Response Code
+ *  reason_code: Reason Code
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_simple_nack(const uint8_t* request_buf,
+                                uint8_t* response_buf, uint16_t response_code,
+                                uint16_t reason_code);
+
+/*
+ * Construct ACK command in the buffer, given the buffer with the
+ * received command, which in this case must be NCSI_GET_VERSION_ID command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  version_id: Version ID struct.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_version_id_ack(const uint8_t* request_buf,
+                                   uint8_t* response_buf,
+                                   const ncsi_version_id_t* version_id);
+
+/*
+ * Construct OEM ACK command in the buffer, given the buffer with the
+ * received OEM command, which in this case must be
+ * NCSI_OEM_COMMAND_GET_HOST_MAC.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  mac: NIC's MAC address.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_oem_get_mac_ack(const uint8_t* request_buf,
+                                    uint8_t* response_buf,
+                                    const mac_addr_t* mac);
+
+/*
+ * Construct simple OEM ACK command in the buffer, given the buffer with the
+ * received command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_oem_simple_ack(const uint8_t* request_buf,
+                                   uint8_t* response_buf);
+
+/*
+ * Construct OEM ACK command in the buffer, given the buffer with the
+ * received command, which in this case must be NCSI_OEM_COMMAND_ECHO.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_oem_echo_ack(const uint8_t* request_buf,
+                                 uint8_t* response_buf);
+
+/*
+ * Construct ACK response in the buffer, given the buffer with the
+ * received NCSI_OEM_COMMAND_GET_FILTER.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  filter: active NIC traffic filter.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_oem_get_filter_ack(const uint8_t* request_buf,
+                                       uint8_t* response_buf,
+                                       const ncsi_oem_filter_t* filter);
+
+/*
+ * Construct ACK response in the buffer, given the buffer with the
+ * received NCSI_GET_PASSTHROUGH_STATISTICS command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  stats: ncsi_passthrough_stats_t struct with stats.
+ *  TODO): it is not clear what is the endianness of the data in stats
+ *  struct. There does not seem to be any conversion on EC side.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_pt_stats_ack(const uint8_t* request_buf,
+                                 uint8_t* response_buf,
+                                 const ncsi_passthrough_stats_t* stats);
+
+/*
+ * This function is similar to ncsi_build_pt_stats_ack, except that it simulates
+ * the bug in MLX X - X firmware. It should not be used outside of tests.
+ */
+uint32_t ncsi_build_pt_stats_legacy_ack(
+    const uint8_t* request_buf, uint8_t* response_buf,
+    const ncsi_passthrough_stats_legacy_t* stats);
+
+/*
+ * Construct ACK response in the buffer, given the buffer with the
+ * received NCSI_GET_LINK_STATUS command.
+ *
+ * Args:
+ *  request_buf: buffer containing NC-SI request.
+ *  response_buf: buffer, where to put the response. Must be big enough to fit
+ *    the response.
+ *  link_status: ncsi_link_status_t struct.
+ *
+ * Returns the size of the response in the buffer.
+ */
+uint32_t ncsi_build_link_status_ack(const uint8_t* request_buf,
+                                    uint8_t* response_buf,
+                                    const ncsi_link_status_t* link_status);
+
+#ifdef __cplusplus
+}  /* extern "C" */
+#endif
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_NCSI_SERVER_H_
diff --git a/ncsid/src/platforms/nemora/portable/net_types.h b/ncsid/src/platforms/nemora/portable/net_types.h
new file mode 100644
index 0000000..872d94f
--- /dev/null
+++ b/ncsid/src/platforms/nemora/portable/net_types.h
@@ -0,0 +1,37 @@
+#ifndef PLATFORMS_NEMORA_PORTABLE_NET_TYPES_H_
+#define PLATFORMS_NEMORA_PORTABLE_NET_TYPES_H_
+
+#include <stdint.h>
+
+// Buffer big enough for largest frame we expect
+// to receive from EMAC (in bytes)
+//   1500 (max payload IEEE 802.3) +
+//   14 (header) +
+//   4 (crc, if not stripped by EMAC) +
+//   4 (optional VLAN tag, if not stripped by EMAC)
+#define ETH_BUFFER_SIZE 1522
+#define IPV4_ETHERTYPE 0x0800
+#define IPV6_ADDR_SIZE 16
+#define MAC_ADDR_SIZE 6
+
+#ifndef __packed
+#define __packed __attribute__((packed))
+#endif
+
+/* MAC address */
+typedef struct __packed {
+  uint8_t octet[MAC_ADDR_SIZE];  // network order
+} mac_addr_t;
+
+/*
+ * Ethernet header.
+ * Note: This assumes a packet without VLAN tags.
+ * TODO: configure HW to strip VLAN field.
+ */
+typedef struct __packed {
+  mac_addr_t dest;
+  mac_addr_t src;
+  uint16_t ethertype;
+} eth_hdr_t;
+
+#endif  // PLATFORMS_NEMORA_PORTABLE_NET_TYPES_H_
diff --git a/ncsid/src/update-static-neighbors@.service.in b/ncsid/src/update-static-neighbors@.service.in
new file mode 100644
index 0000000..c729cc5
--- /dev/null
+++ b/ncsid/src/update-static-neighbors@.service.in
@@ -0,0 +1,16 @@
+[Unit]
+Description=Static Neighbor Updater
+Wants=mapper-wait@-xyz-openbmc_project-network-%i.service
+After=mapper-wait@-xyz-openbmc_project-network-%i.service
+Requisite=nic-hostless@%i.target
+After=nic-hostless@%i.target
+BindsTo=nic-hostless@%i.target
+StartLimitIntervalSec=1min
+StartLimitBurst=5
+
+[Service]
+KillMode=mixed
+Restart=on-failure
+ExecStart=@@BIN@ update-static-neighbors %I
+SyslogIdentifier=update-static-neighbors@%I
+SuccessExitStatus=10
diff --git a/ncsid/src/update-static-neighbors@.timer b/ncsid/src/update-static-neighbors@.timer
new file mode 100644
index 0000000..883780e
--- /dev/null
+++ b/ncsid/src/update-static-neighbors@.timer
@@ -0,0 +1,8 @@
+[Unit]
+Description=Retry neighbor update periodically
+Requisite=nic-hostless@%i.target
+After=nic-hostless@%i.target
+BindsTo=nic-hostless@%i.target
+
+[Timer]
+OnUnitInactiveSec=1min
diff --git a/ncsid/src/update_static_neighbors.sh b/ncsid/src/update_static_neighbors.sh
new file mode 100644
index 0000000..711ae4b
--- /dev/null
+++ b/ncsid/src/update_static_neighbors.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
+
+UpdateNeighbor() {
+  local netdev="$1"
+  local service="$2"
+  local object="$3"
+
+  eval "$(GetNeighbor "$service" "$object" | JSONToVars)" || return $?
+  local new_mac
+  if ! new_mac="$(DetermineNeighbor "$netdev" "$IPAddress")"; then
+    echo "Faild to find $IPAddress" >&2
+    return 10
+  fi
+  new_mac=$(normalize_mac "$new_mac") || return
+  if [ "$new_mac" != "$(normalize_mac "$MACAddress")" ]; then
+    echo "Updating $IPAddress: $MACAddress -> $new_mac" >&2
+    SuppressTerm
+    local rc=0
+    DeleteObject "$service" "$object" && \
+      AddNeighbor "$service" "$netdev" "$IPAddress" "$new_mac" || \
+      rc=$?
+    UnsuppressTerm
+    return $rc
+  fi
+}
+
+UpdateNeighbors() {
+  local netdev="$1"
+
+  local entry
+  while read entry; do
+    eval "$(echo "$entry" | JSONToVars)"
+    RunInterruptibleBg UpdateNeighbor "$netdev" "$service" "$object"
+  done < <(GetNeighborObjects "$netdev" 2>/dev/null)
+  WaitInterruptibleBg
+}
+
+Main() {
+  set -o nounset
+  set -o errexit
+  set -o pipefail
+
+  InitTerm
+  UpdateNeighbors "$@"
+}
+
+return 0 2>/dev/null
+Main "$@"
diff --git a/ncsid/subprojects b/ncsid/subprojects
new file mode 120000
index 0000000..d2458e9
--- /dev/null
+++ b/ncsid/subprojects
@@ -0,0 +1 @@
+../subprojects/
\ No newline at end of file
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
+}
diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap
new file mode 100644
index 0000000..6847ae5
--- /dev/null
+++ b/subprojects/fmt.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/fmtlib/fmt
+revision = HEAD
diff --git a/subprojects/ncsid b/subprojects/ncsid
new file mode 120000
index 0000000..782e463
--- /dev/null
+++ b/subprojects/ncsid
@@ -0,0 +1 @@
+../ncsid
\ No newline at end of file
diff --git a/subprojects/sdbusplus.wrap b/subprojects/sdbusplus.wrap
new file mode 100644
index 0000000..7f736e7
--- /dev/null
+++ b/subprojects/sdbusplus.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/openbmc/sdbusplus
+revision = HEAD
diff --git a/subprojects/stdplus.wrap b/subprojects/stdplus.wrap
new file mode 100644
index 0000000..00dae65
--- /dev/null
+++ b/subprojects/stdplus.wrap
@@ -0,0 +1,3 @@
+[wrap-git]
+url = https://github.com/openbmc/stdplus
+revision = HEAD