[netsvc] Partial rewrite and add tests

Also call directly into paving service rather go through
install-disk-image.

Tested: runtests -t netsvc-test
Tested: Paved astro, sherlock, nuc, pixelbook

Change-Id: I8976b773e8a00baf5438a987e68f46d96f510ec8
diff --git a/zircon/system/core/netsvc/BUILD.gn b/zircon/system/core/netsvc/BUILD.gn
index f827b07..c47a522 100644
--- a/zircon/system/core/netsvc/BUILD.gn
+++ b/zircon/system/core/netsvc/BUILD.gn
@@ -6,14 +6,9 @@
 
 executable("netsvc") {
   sources = [
-    "board-name.cpp",
     "debuglog.cpp",
     "device_id.cpp",
-    "netboot.cpp",
-    "netfile.cpp",
     "netsvc.cpp",
-    "tftp.cpp",
-    "zbi.cpp",
   ]
   if (enable_netsvc_debugging_features) {
     sources += [
@@ -25,27 +20,71 @@
     ]
   }
   deps = [
-    "$zx/system/fidl/fuchsia-device:c",
-    "$zx/system/fidl/fuchsia-device-manager:c",
-    "$zx/system/fidl/fuchsia-hardware-block:c",
+    ":netsvc_common",
     "$zx/system/fidl/fuchsia-hardware-ethernet:c",
-    "$zx/system/fidl/fuchsia-sysinfo:c",
-    "$zx/system/ulib/chromeos-disk-setup",
     "$zx/system/ulib/fdio",
-    "$zx/system/ulib/fzl",
-    "$zx/system/ulib/gpt",
     "$zx/system/ulib/inet6",
-    "$zx/system/ulib/libzbi",
     "$zx/system/ulib/sync",
     "$zx/system/ulib/tftp",
     "$zx/system/ulib/zircon",
     "$zx/system/ulib/zx",
   ]
   data_deps = [
-    # netsvc launches /boot/bin/install-disk-image under --netboot.
-    "$zx/system/uapp/disk-pave",
-
     # netsvc launches /boot/bin/sh for netruncmd.
     "$zx/third_party/uapp/dash",
   ]
 }
+
+source_set("netsvc_common") {
+  sources = [
+    "board-name.cpp",
+    "file-api.cpp",
+    "netcp.cpp",
+    "netboot.cpp",
+    "paver.cpp",
+    "payload-streamer.cpp",
+    "tftp.cpp",
+    "zbi.cpp",
+  ]
+  deps = [
+    "$zx/system/fidl/fuchsia-device:c",
+    "$zx/system/fidl/fuchsia-device-manager:c",
+    "$zx/system/fidl/fuchsia-hardware-block:c",
+    "$zx/system/fidl/fuchsia-sysinfo:c",
+    "$zx/system/ulib/chromeos-disk-setup",
+    "$zx/system/ulib/async-loop:async-loop-cpp",
+    "$zx/system/ulib/fdio",
+    "$zx/system/ulib/gpt",
+    "$zx/system/ulib/libzbi",
+    "$zx/system/ulib/zircon",
+  ]
+  public_deps = [
+    "$zx/system/fidl/fuchsia-paver:c",
+    "$zx/system/ulib/fbl",
+    "$zx/system/ulib/fidl-utils",
+    "$zx/system/ulib/fzl",
+    "$zx/system/ulib/inet6",
+    "$zx/system/ulib/sync",
+    "$zx/system/ulib/tftp",
+    "$zx/system/ulib/zx",
+  ]
+}
+
+test("netsvc-test") {
+  output_name = "netsvc-test"
+  sources = [
+    "test/file-api-test.cpp",
+    "test/paver-test.cpp",
+    "test/payload-streamer-test.cpp",
+    "test/tftp-test.cpp",
+  ]
+  include_dirs = [ "." ]
+  deps = [
+    ":netsvc_common",
+    "$zx/system/fidl/fuchsia-sysinfo:c",
+    "$zx/system/ulib/async-loop:async-loop-cpp",
+    "$zx/system/ulib/fidl-utils",
+    "$zx/system/ulib/fs",
+    "$zx/system/ulib/zxtest",
+  ]
+}
diff --git a/zircon/system/core/netsvc/board-name.cpp b/zircon/system/core/netsvc/board-name.cpp
index 3de5059..9537cac 100644
--- a/zircon/system/core/netsvc/board-name.cpp
+++ b/zircon/system/core/netsvc/board-name.cpp
@@ -19,7 +19,6 @@
 #include <gpt/gpt.h>
 #include <lib/fdio/directory.h>
 #include <lib/fzl/fdio.h>
-#include <lib/zx/channel.h>
 #include <zircon/status.h>
 
 #include <algorithm>
@@ -30,12 +29,12 @@
     constexpr char kBlockDevPath[] = "/dev/class/block/";
     fbl::unique_fd d_fd(open(kBlockDevPath, O_RDONLY));
     if (!d_fd) {
-        printf("netsvc: Cannot inspect block devices\n");
+        fprintf(stderr, "netsvc: Cannot inspect block devices\n");
         return fbl::unique_fd();
     }
     DIR* d = fdopendir(d_fd.release());
     if (d == nullptr) {
-        printf("netsvc: Cannot inspect block devices\n");
+        fprintf(stderr, "netsvc: Cannot inspect block devices\n");
         return fbl::unique_fd();
     }
     const auto closer = fbl::MakeAutoCall([&]() { closedir(d); });
@@ -95,14 +94,15 @@
         status = io_status;
     }
     if (status != ZX_OK) {
-        printf("netsvc: Could not acquire GPT block info: %s\n", zx_status_get_string(status));
+        fprintf(stderr, "netsvc: Could not acquire GPT block info: %s\n",
+                zx_status_get_string(status));
         return false;
     }
     fbl::unique_ptr<gpt::GptDevice> gpt;
     status = gpt::GptDevice::Create(gpt_fd.get(), block_info.block_size, block_info.block_count,
                                     &gpt);
     if (status != ZX_OK) {
-        printf("netsvc: Failed to get GPT info: %s\n", zx_status_get_string(status));
+        fprintf(stderr, "netsvc: Failed to get GPT info: %s\n", zx_status_get_string(status));
         return false;
     }
     return is_cros(gpt.get());
@@ -110,24 +110,18 @@
 
 } // namespace
 
-bool check_board_name(const char* name, size_t length) {
-    length = std::min(length, ZX_MAX_NAME_LEN);
-
-    constexpr char kSysInfoPath[] = "/dev/misc/sysinfo";
-    fbl::unique_fd sysinfo(open(kSysInfoPath, O_RDWR));
+bool CheckBoardName(const zx::channel& sysinfo, const char* name, size_t length) {
     if (!sysinfo) {
         return false;
     }
-    zx::channel channel;
-    if (fdio_get_service_handle(sysinfo.release(), channel.reset_and_get_address()) != ZX_OK) {
-        return false;
-    }
+
+    length = std::min(length, ZX_MAX_NAME_LEN);
 
     char real_board_name[ZX_MAX_NAME_LEN] = {};
     zx_status_t status;
     size_t actual_size;
     zx_status_t fidl_status = fuchsia_sysinfo_DeviceGetBoardName(
-        channel.get(), &status, real_board_name, sizeof(real_board_name), &actual_size);
+        sysinfo.get(), &status, real_board_name, sizeof(real_board_name), &actual_size);
     if (fidl_status != ZX_OK || status != ZX_OK) {
         return false;
     }
diff --git a/zircon/system/core/netsvc/board-name.h b/zircon/system/core/netsvc/board-name.h
index f4c4c68d..61d641c 100644
--- a/zircon/system/core/netsvc/board-name.h
+++ b/zircon/system/core/netsvc/board-name.h
@@ -6,4 +6,6 @@
 
 #include <unistd.h>
 
-bool check_board_name(const char* name, size_t length);
+#include <lib/zx/channel.h>
+
+bool CheckBoardName(const zx::channel& sysinfo, const char* name, size_t length);
diff --git a/zircon/system/core/netsvc/debuglog.cpp b/zircon/system/core/netsvc/debuglog.cpp
index 292df74..f9cd11e 100644
--- a/zircon/system/core/netsvc/debuglog.cpp
+++ b/zircon/system/core/netsvc/debuglog.cpp
@@ -2,13 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "debuglog.h"
+
 #include <inttypes.h>
 #include <string.h>
+#include <stdio.h>
 
+#include <zircon/boot/netboot.h>
 #include <zircon/syscalls.h>
 #include <zircon/syscalls/log.h>
 
 #include "netsvc.h"
+#include "tftp.h"
 
 #define MAX_LOG_LINE (ZX_LOG_RECORD_MAX + 32)
 
@@ -19,7 +24,9 @@
 static volatile uint32_t seqno = 1;
 static volatile uint32_t pending = 0;
 
-zx_time_t debuglog_next_timeout = ZX_TIME_INFINITE;
+static zx_time_t g_debuglog_next_timeout = ZX_TIME_INFINITE;
+
+zx_time_t debuglog_next_timeout() { return g_debuglog_next_timeout; }
 
 #define SEND_DELAY_SHORT ZX_MSEC(100)
 #define SEND_DELAY_LONG ZX_SEC(4)
@@ -63,7 +70,7 @@
     }
 
     // Set up our timeout to expire immediately, so that we check for pending log messages
-    debuglog_next_timeout = zx_clock_get_monotonic();
+    g_debuglog_next_timeout = zx_clock_get_monotonic();
 
     seqno = 1;
     pending = 0;
@@ -77,7 +84,7 @@
     if (pending == 0) {
         pkt.magic = NB_DEBUGLOG_MAGIC;
         pkt.seqno = seqno;
-        strncpy(pkt.nodename, nodename, sizeof(pkt.nodename) - 1);
+        strncpy(pkt.nodename, nodename(), sizeof(pkt.nodename) - 1);
         pkt_len = 0;
         while (pkt_len < (MAX_LOG_DATA - MAX_LOG_LINE)) {
             size_t r = get_log_line(pkt.data + pkt_len);
@@ -97,7 +104,7 @@
     }
     udp6_send(&pkt, pkt_len, &ip6_ll_all_nodes, DEBUGLOG_PORT, DEBUGLOG_ACK_PORT, false);
 done:
-    debuglog_next_timeout = zx_deadline_after(send_delay);
+    g_debuglog_next_timeout = zx_deadline_after(send_delay);
 }
 
 void debuglog_recv(void* data, size_t len, bool is_mcast) {
diff --git a/zircon/system/core/netsvc/debuglog.h b/zircon/system/core/netsvc/debuglog.h
new file mode 100644
index 0000000..00f26e1
--- /dev/null
+++ b/zircon/system/core/netsvc/debuglog.h
@@ -0,0 +1,17 @@
+// Copyright 2016 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <zircon/types.h>
+
+zx_time_t debuglog_next_timeout();
+
+int debuglog_init();
+
+void debuglog_recv(void* data, size_t len, bool is_mcast);
+
+void debuglog_timeout_expired();
+
+
diff --git a/zircon/system/core/netsvc/file-api.cpp b/zircon/system/core/netsvc/file-api.cpp
new file mode 100644
index 0000000..761b319
--- /dev/null
+++ b/zircon/system/core/netsvc/file-api.cpp
@@ -0,0 +1,174 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "file-api.h"
+
+#include <errno.h>
+#include <fcntl.h>
+
+#include <fbl/unique_fd.h>
+#include <lib/fdio/fdio.h>
+#include <zircon/boot/netboot.h>
+
+#include "board-name.h"
+#include "netboot.h"
+
+namespace netsvc {
+namespace {
+
+size_t NB_IMAGE_PREFIX_LEN() {
+    return strlen(NB_IMAGE_PREFIX);
+}
+size_t NB_FILENAME_PREFIX_LEN() {
+    return strlen(NB_FILENAME_PREFIX);
+}
+
+} // namespace
+
+FileApi::FileApi(bool is_zedboot, std::unique_ptr<NetCopyInterface> netcp, zx::channel sysinfo,
+                 PaverInterface* paver)
+    : is_zedboot_(is_zedboot), sysinfo_(std::move(sysinfo)), netcp_(std::move(netcp)),
+      paver_(paver) {
+    ZX_ASSERT(paver_ != nullptr);
+
+    if (!sysinfo_) {
+        constexpr char kSysInfoPath[] = "/dev/misc/sysinfo";
+        fbl::unique_fd sysinfo_fd(open(kSysInfoPath, O_RDWR));
+        if (sysinfo_fd) {
+            fdio_get_service_handle(sysinfo_fd.release(), sysinfo_.reset_and_get_address());
+        }
+    }
+}
+
+ssize_t FileApi::OpenRead(const char* filename) {
+    // Make sure all in-progress paving operations have completed
+    if (paver_->InProgress() == true) {
+        return TFTP_ERR_SHOULD_WAIT;
+    }
+    if (paver_->exit_code() != ZX_OK) {
+        fprintf(stderr, "paver exited with error: %d\n", paver_->exit_code());
+        paver_->reset_exit_code();
+        return TFTP_ERR_IO;
+    }
+
+    is_write_ = false;
+    strncpy(filename_, filename, PATH_MAX);
+    filename_[PATH_MAX] = '\0';
+    netboot_file_ = NULL;
+    size_t file_size;
+    if (netcp_->Open(filename, O_RDONLY, &file_size) == 0) {
+        return static_cast<ssize_t>(file_size);
+    }
+    return TFTP_ERR_NOT_FOUND;
+}
+
+tftp_status FileApi::OpenWrite(const char* filename, size_t size) {
+    // Make sure all in-progress paving operations have completed
+    if (paver_->InProgress() == true) {
+        return TFTP_ERR_SHOULD_WAIT;
+    }
+    if (paver_->exit_code() != ZX_OK) {
+        fprintf(stderr, "paver exited with error: %d\n", paver_->exit_code());
+        paver_->reset_exit_code();
+        return TFTP_ERR_IO;
+    }
+
+    is_write_ = true;
+    strncpy(filename_, filename, PATH_MAX);
+    filename_[PATH_MAX] = '\0';
+
+    if (is_zedboot_ && !strncmp(filename_, NB_FILENAME_PREFIX, NB_FILENAME_PREFIX_LEN())) {
+        type_ = NetfileType::kNetboot;
+        netboot_file_ = netboot_get_buffer(filename_, size);
+        if (netboot_file_ != NULL) {
+            return TFTP_NO_ERROR;
+        }
+    } else if (is_zedboot_ && !strcmp(filename_, NB_BOARD_NAME_FILENAME)) {
+        printf("netsvc: Running board name validation\n");
+        type_ = NetfileType::kBoardName;
+        return TFTP_NO_ERROR;
+    } else if (is_zedboot_ && !strncmp(filename_, NB_IMAGE_PREFIX, NB_IMAGE_PREFIX_LEN())) {
+        type_ = NetfileType::kPaver;
+        tftp_status status = paver_->OpenWrite(filename_, size);
+        if (status != TFTP_NO_ERROR) {
+            filename_[0] = '\0';
+        }
+        return status;
+    } else {
+        type_ = NetfileType::kNetCopy;
+        if (netcp_->Open(filename_, O_WRONLY, NULL) == 0) {
+            return TFTP_NO_ERROR;
+        }
+    }
+    return TFTP_ERR_INVALID_ARGS;
+}
+
+tftp_status FileApi::Read(void* data, size_t* length, off_t offset) {
+    if (length == NULL) {
+        return TFTP_ERR_INVALID_ARGS;
+    }
+    ssize_t read_len = netcp_->Read(data, offset, *length);
+    if (read_len < 0) {
+        return TFTP_ERR_IO;
+    }
+    *length = static_cast<size_t>(read_len);
+    return TFTP_NO_ERROR;
+}
+
+tftp_status FileApi::Write(const void* data, size_t* length, off_t offset) {
+    if (length == NULL) {
+        return TFTP_ERR_INVALID_ARGS;
+    }
+    switch (type_) {
+    case NetfileType::kNetboot: {
+        nbfile* nb_file = netboot_file_;
+        if ((static_cast<size_t>(offset) > nb_file->size) || (offset + *length) > nb_file->size) {
+            return TFTP_ERR_INVALID_ARGS;
+        }
+        memcpy(nb_file->data + offset, data, *length);
+        nb_file->offset = offset + *length;
+        return TFTP_NO_ERROR;
+    }
+    case NetfileType::kPaver:
+        return paver_->Write(data, length, offset);
+
+    case NetfileType::kBoardName:
+        return CheckBoardName(sysinfo_, reinterpret_cast<const char*>(data), *length)
+                   ? TFTP_NO_ERROR
+                   : TFTP_ERR_BAD_STATE;
+    case NetfileType::kNetCopy: {
+        ssize_t write_result =
+            netcp_->Write(reinterpret_cast<const char*>(data), offset, *length);
+        if (static_cast<size_t>(write_result) == *length) {
+            return TFTP_NO_ERROR;
+        }
+        if (write_result == -EBADF) {
+            return TFTP_ERR_BAD_STATE;
+        }
+        return TFTP_ERR_IO;
+    }
+    default:
+        return ZX_ERR_BAD_STATE;
+    }
+
+    return TFTP_ERR_BAD_STATE;
+}
+
+void FileApi::Close() {
+    if (type_ == NetfileType::kNetCopy) {
+        netcp_->Close();
+    } else if (type_ == NetfileType::kPaver) {
+        paver_->Close();
+    }
+    type_ = NetfileType::kUnknown;
+}
+
+void FileApi::Abort() {
+    if (is_write_ && type_ == NetfileType::kNetCopy) {
+        netcp_->AbortWrite();
+    }
+    Close();
+}
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/file-api.h b/zircon/system/core/netsvc/file-api.h
new file mode 100644
index 0000000..f820d40
--- /dev/null
+++ b/zircon/system/core/netsvc/file-api.h
@@ -0,0 +1,95 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <tftp/tftp.h>
+#include <zircon/boot/netboot.h>
+
+#include "netcp.h"
+#include "paver.h"
+
+namespace netsvc {
+
+// Provides capabilities to read/write files sent over TFTP.
+//
+// Reads only implements netcp. Specifically it enables reading of files in
+// global /data.
+//
+// Writes come in 4 flavors:
+// * netcp: Ability to write to global /data.
+// * netboot: Mexec into image once write completes.
+// * paving: Writes boot partions, or FVM.
+// * board name validation: Validates that board name sent matches current
+//   board.
+class FileApiInterface {
+public:
+    // Returns size of file on success.
+    virtual ssize_t OpenRead(const char* filename) = 0;
+    virtual tftp_status OpenWrite(const char* filename, size_t size) = 0;
+    virtual tftp_status Read(void* data, size_t* length, off_t offset) = 0;
+    virtual tftp_status Write(const void* data, size_t* length, off_t offset) = 0;
+    virtual void Close() = 0;
+    // Like close, but signals read or write operation was incomplete.
+    virtual void Abort() = 0;
+
+    virtual bool is_write() = 0;
+    virtual const char* filename() = 0;
+};
+
+class FileApi : public FileApiInterface {
+public:
+    // FileApi does *not* take ownership of |paver|.
+    explicit FileApi(bool is_zedboot,
+                     std::unique_ptr<NetCopyInterface> netcp = std::make_unique<NetCopy>(),
+                     zx::channel sysinfo = zx::channel(),
+                     PaverInterface* paver = Paver::Get());
+
+    ssize_t OpenRead(const char* filename) final;
+    tftp_status OpenWrite(const char* filename, size_t size) final;
+    tftp_status Read(void* data, size_t* length, off_t offset) final;
+    tftp_status Write(const void* data, size_t* length, off_t offset) final;
+    void Close() final;
+    void Abort() final;
+
+    const char* filename() final {
+        return filename_;
+    }
+
+    bool is_write() final {
+        return is_write_;
+    }
+
+private:
+    // Identifies what the file being streamed over TFTP should be
+    // used for.
+    enum class NetfileType {
+        kUnknown,   // No reads/writes currently in progress.
+        kNetCopy,   // A file in /data
+        kNetboot,   // A bootfs file
+        kPaver,     // A disk image which should be paved to disk
+        kBoardName, // A file containing the board name.
+                    // Expected to return error if it doesn't match the current board name.
+    };
+
+    bool is_zedboot_;
+
+    bool is_write_ = false;
+    char filename_[PATH_MAX + 1] = {};
+    NetfileType type_ = NetfileType::kUnknown;
+
+    // Use when type_ == NetfileType::kBoardName.
+    zx::channel sysinfo_;
+
+    // Used when type_ == NetfileType::kNetCopy.
+    std::unique_ptr<NetCopyInterface> netcp_;
+
+    // Only valid when type_ == NetfileType::kNetboot.
+    nbfile* netboot_file_ = nullptr;
+
+    // Used when type_ == NetfileType::kPaver.
+    PaverInterface* paver_;
+};
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/netboot.cpp b/zircon/system/core/netsvc/netboot.cpp
index a2abbd7..6b762d1 100644
--- a/zircon/system/core/netsvc/netboot.cpp
+++ b/zircon/system/core/netsvc/netboot.cpp
@@ -2,8 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "netsvc.h"
-#include "zbi.h"
+#include "netboot.h"
 
 #include <assert.h>
 #include <errno.h>
@@ -26,6 +25,11 @@
 #include <zircon/processargs.h>
 #include <zircon/syscalls.h>
 
+#include "netsvc.h"
+#include "netcp.h"
+#include "paver.h"
+#include "zbi.h"
+
 static uint32_t last_cookie = 0;
 static uint32_t last_cmd = 0;
 static uint32_t last_arg = 0;
@@ -155,10 +159,15 @@
     m.magic = NB_MAGIC;
     m.cookie = cookie;
     m.cmd = NB_ACK;
-    m.arg = netfile_open(filename, arg, NULL);
+    m.arg = netcp_open(filename, arg, NULL);
     udp6_send(&m, sizeof(m), saddr, sport, dport, false);
 }
 
+struct netfilemsg {
+    nbmsg hdr;
+    uint8_t data[1024];
+};
+
 static void nb_read(uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr, uint16_t sport,
                     uint16_t dport) {
     static netfilemsg m = {
@@ -182,7 +191,7 @@
             msg_size = sizeof(m.hdr);
         }
     } else if (arg == 0 || arg == blocknum + 1) {
-        ssize_t result = netfile_read(&m.data, sizeof(m.data));
+        ssize_t result = netcp_read(&m.data, sizeof(m.data));
         if (result < 0) {
             m.hdr.arg = static_cast<uint32_t>(result);
             msg_size = sizeof(m.hdr);
@@ -218,7 +227,7 @@
             m.arg = -EIO;
         }
     } else if (arg == 0 || arg == blocknum + 1) {
-        ssize_t result = netfile_write(data, len);
+        ssize_t result = netcp_write(data, len);
         m.arg = static_cast<uint32_t>(result > 0 ? 0 : result);
         blocknum = arg;
     }
@@ -231,7 +240,7 @@
     m.magic = NB_MAGIC;
     m.cookie = cookie;
     m.cmd = NB_ACK;
-    m.arg = netfile_close();
+    m.arg = netcp_close();
     udp6_send(&m, sizeof(m), saddr, sport, dport, false);
 }
 
@@ -391,12 +400,12 @@
         break;
     case NB_BOOT:
         // Wait for the paver to complete
-        while (atomic_load(&paving_in_progress)) {
+        while (netsvc::Paver::Get()->InProgress()) {
             thrd_yield();
         }
-        if (atomic_load(&paver_exit_code) != 0) {
-            printf("netboot: detected paver error: %d\n", atomic_load(&paver_exit_code));
-            atomic_store(&paver_exit_code, 0);
+        if (netsvc::Paver::Get()->exit_code() != 0) {
+            printf("netboot: detected paver error: %d\n", netsvc::Paver::Get()->exit_code());
+            netsvc::Paver::Get()->reset_exit_code();
             break;
         }
         do_boot = true;
@@ -404,12 +413,12 @@
         break;
     case NB_REBOOT:
         // Wait for the paver to complete
-        while (atomic_load(&paving_in_progress)) {
+        while (netsvc::Paver::Get()->InProgress()) {
             thrd_yield();
         }
-        if (atomic_load(&paver_exit_code) != 0) {
-            printf("netboot: detected paver error: %d\n", atomic_load(&paver_exit_code));
-            atomic_store(&paver_exit_code, 0);
+        if (netsvc::Paver::Get()->exit_code() != 0) {
+            printf("netboot: detected paver error: %d\n", netsvc::Paver::Get()->exit_code());
+            netsvc::Paver::Get()->reset_exit_code();
             break;
         }
         do_reboot = true;
@@ -468,17 +477,17 @@
     switch (msg->cmd) {
     case NB_QUERY: {
         if (strcmp(reinterpret_cast<char*>(msg->data), "*") &&
-            strcmp(reinterpret_cast<char*>(msg->data), nodename)) {
+            strcmp(reinterpret_cast<char*>(msg->data), nodename())) {
             break;
         }
-        size_t dlen = strlen(nodename) + 1;
+        size_t dlen = strlen(nodename()) + 1;
         char buf[1024 + sizeof(nbmsg)];
         if ((dlen + sizeof(nbmsg)) > sizeof(buf)) {
             return;
         }
         msg->cmd = NB_ACK;
         memcpy(buf, msg, sizeof(nbmsg));
-        memcpy(buf + sizeof(nbmsg), nodename, dlen);
+        memcpy(buf + sizeof(nbmsg), nodename(), dlen);
         udp6_send(buf, sizeof(nbmsg) + dlen, saddr, sport, dport, false);
         break;
     }
@@ -505,7 +514,7 @@
     default:
         // If the bootloader is enabled, then let it have a crack at the
         // incoming packets as well.
-        if (netbootloader) {
+        if (netbootloader()) {
             bootloader_recv(data, len + sizeof(nbmsg), daddr, dport, saddr, sport);
         }
     }
diff --git a/zircon/system/core/netsvc/netboot.h b/zircon/system/core/netsvc/netboot.h
new file mode 100644
index 0000000..8440ded
--- /dev/null
+++ b/zircon/system/core/netsvc/netboot.h
@@ -0,0 +1,14 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <inet6/inet6.h>
+
+void netboot_advertise(const char* nodename);
+
+void netboot_recv(void* data, size_t len, bool is_mcast, const ip6_addr_t* daddr, uint16_t dport,
+                  const ip6_addr_t* saddr, uint16_t sport);
+
+void netboot_run_cmd(const char* cmd);
diff --git a/zircon/system/core/netsvc/netcp.cpp b/zircon/system/core/netsvc/netcp.cpp
new file mode 100644
index 0000000..4a97a0e
--- /dev/null
+++ b/zircon/system/core/netsvc/netcp.cpp
@@ -0,0 +1,234 @@
+// Copyright 2016 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "netcp.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <inet6/inet6.h>
+#include <inet6/netifc.h>
+
+#include <zircon/processargs.h>
+#include <zircon/syscalls.h>
+
+#include <zircon/boot/netboot.h>
+
+namespace {
+
+constexpr char TMP_SUFFIX[] = ".netsvc.tmp";
+
+struct netcp_state {
+    int fd;
+    off_t offset;
+    // false: Filename is the open file and final destination
+    // true : Filename is final destination; open file has a magic tmp suffix
+    bool needs_rename;
+    char filename[PATH_MAX];
+};
+
+netcp_state netcp = {
+    .fd = -1,
+    .offset = 0,
+    .needs_rename = false,
+    .filename = {0},
+};
+
+int netcp_mkdir(const char* filename) {
+    const char* ptr = filename[0] == '/' ? filename + 1 : filename;
+    struct stat st;
+    char tmp[1024];
+    for (;;) {
+        ptr = strchr(ptr, '/');
+        if (!ptr) {
+            return 0;
+        }
+        memcpy(tmp, filename, ptr - filename);
+        tmp[ptr - filename] = '\0';
+        ptr += 1;
+        if (stat(tmp, &st) < 0) {
+            if (errno == ENOENT) {
+                if (mkdir(tmp, 0755) < 0) {
+                    return -1;
+                }
+            } else {
+                return -1;
+            }
+        }
+    }
+}
+
+} // namespace
+
+int netcp_open(const char* filename, uint32_t arg, size_t* file_size) {
+    if (netcp.fd >= 0) {
+        printf("netsvc: closing still-open '%s', replacing with '%s'\n", netcp.filename,
+               filename);
+        close(netcp.fd);
+        netcp.fd = -1;
+    }
+    size_t len = strlen(filename);
+    strlcpy(netcp.filename, filename, sizeof(netcp.filename));
+
+    struct stat st;
+again: // label here to catch filename=/path/to/new/directory/
+    if (stat(filename, &st) == 0 && S_ISDIR(st.st_mode)) {
+        errno = EISDIR;
+        goto err;
+    }
+
+    switch (arg) {
+    case O_RDONLY:
+        netcp.needs_rename = false;
+        netcp.fd = open(filename, O_RDONLY);
+        if (file_size) {
+            *file_size = st.st_size;
+        }
+        break;
+    case O_WRONLY: {
+        // If we're writing a file, actually write to "filename + TMP_SUFFIX",
+        // and rename to the final destination when we would close. This makes
+        // written files appear to atomically update.
+        if (len + strlen(TMP_SUFFIX) + 1 > PATH_MAX) {
+            errno = ENAMETOOLONG;
+            goto err;
+        }
+        strcat(netcp.filename, TMP_SUFFIX);
+        netcp.needs_rename = true;
+        netcp.fd = open(netcp.filename, O_WRONLY | O_CREAT | O_TRUNC);
+        netcp.filename[len] = '\0';
+        if (netcp.fd < 0 && errno == ENOENT) {
+            if (netcp_mkdir(filename) == 0) {
+                goto again;
+            }
+        }
+        break;
+    }
+    default:
+        printf("netsvc: open '%s' with invalid mode %d\n", filename, arg);
+        errno = EINVAL;
+    }
+    if (netcp.fd < 0) {
+        goto err;
+    } else {
+        strlcpy(netcp.filename, filename, sizeof(netcp.filename));
+        netcp.offset = 0;
+    }
+
+    return 0;
+err:
+    netcp.filename[0] = '\0';
+    return -errno;
+}
+
+ssize_t netcp_offset_read(void* data_out, off_t offset, size_t max_len) {
+    if (netcp.fd < 0) {
+        printf("netsvc: read, but no open file\n");
+        return -EBADF;
+    }
+    if (offset != netcp.offset) {
+        if (lseek(netcp.fd, offset, SEEK_SET) != offset) {
+            return -errno;
+        }
+        netcp.offset = offset;
+    }
+    return netcp_read(data_out, max_len);
+}
+
+ssize_t netcp_read(void* data_out, size_t data_sz) {
+    if (netcp.fd < 0) {
+        printf("netsvc: read, but no open file\n");
+        return -EBADF;
+    }
+    ssize_t n = read(netcp.fd, data_out, data_sz);
+    if (n < 0) {
+        printf("netsvc: error reading '%s': %d\n", netcp.filename, errno);
+        int result = (errno == 0) ? -EIO : -errno;
+        close(netcp.fd);
+        netcp.fd = -1;
+        return result;
+    }
+    netcp.offset += n;
+    return n;
+}
+
+ssize_t netcp_offset_write(const char* data, off_t offset, size_t length) {
+    if (netcp.fd < 0) {
+        printf("netsvc: write, but no open file\n");
+        return -EBADF;
+    }
+    if (offset != netcp.offset) {
+        if (lseek(netcp.fd, offset, SEEK_SET) != offset) {
+            return -errno;
+        }
+        netcp.offset = offset;
+    }
+    return netcp_write(data, length);
+}
+
+ssize_t netcp_write(const char* data, size_t len) {
+    if (netcp.fd < 0) {
+        printf("netsvc: write, but no open file\n");
+        return -EBADF;
+    }
+    ssize_t n = write(netcp.fd, data, len);
+    if (n != static_cast<ssize_t>(len)) {
+        printf("netsvc: error writing %s: %d\n", netcp.filename, errno);
+        int result = (errno == 0) ? -EIO : -errno;
+        close(netcp.fd);
+        netcp.fd = -1;
+        return result;
+    }
+    netcp.offset += len;
+    return len;
+}
+
+int netcp_close() {
+    int result = 0;
+    if (netcp.fd < 0) {
+        printf("netsvc: close, but no open file\n");
+    } else {
+        if (netcp.needs_rename) {
+            char src[PATH_MAX];
+            strlcpy(src, netcp.filename, sizeof(src));
+            strlcat(src, TMP_SUFFIX, sizeof(src));
+            if (rename(src, netcp.filename)) {
+                printf("netsvc: failed to rename temporary file: %s\n", strerror(errno));
+            }
+        }
+        if (close(netcp.fd)) {
+            result = (errno == 0) ? -EIO : -errno;
+        }
+        netcp.fd = -1;
+    }
+    return result;
+}
+
+// Clean up if we abort before finishing a write. Close out and unlink it, rather than
+// leaving an incomplete file.
+void netcp_abort_write() {
+    if (netcp.fd < 0) {
+        return;
+    }
+    close(netcp.fd);
+    netcp.fd = -1;
+    char tmp[PATH_MAX];
+    const char* filename;
+    if (netcp.needs_rename) {
+        strlcpy(tmp, netcp.filename, sizeof(tmp));
+        strlcat(tmp, TMP_SUFFIX, sizeof(tmp));
+        filename = tmp;
+    } else {
+        filename = netcp.filename;
+    }
+    if (unlink(filename) != 0) {
+        printf("netsvc: failed to unlink aborted file %s\n", filename);
+    }
+}
diff --git a/zircon/system/core/netsvc/netcp.h b/zircon/system/core/netsvc/netcp.h
new file mode 100644
index 0000000..8a698bb
--- /dev/null
+++ b/zircon/system/core/netsvc/netcp.h
@@ -0,0 +1,72 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <optional>
+
+#include <sys/types.h>
+#include <zircon/types.h>
+
+int netcp_open(const char* filename, uint32_t arg, size_t* file_size);
+
+ssize_t netcp_offset_read(void* data_out, off_t offset, size_t max_len);
+
+ssize_t netcp_read(void* data_out, size_t data_sz);
+
+ssize_t netcp_offset_write(const char* data, off_t offset, size_t length);
+
+ssize_t netcp_write(const char* data, size_t len);
+
+int netcp_close();
+
+void netcp_abort_write();
+
+
+namespace netsvc {
+
+class NetCopyInterface {
+public:
+    virtual ~NetCopyInterface() {}
+    virtual int Open(const char* filename, uint32_t arg, size_t* file_size) = 0;
+    virtual ssize_t Read(void* data_out, std::optional<off_t> offset, size_t max_len) = 0;
+    virtual ssize_t Write(const char* data, std::optional<off_t> offset, size_t length) = 0;
+    virtual int Close() = 0;
+    virtual void AbortWrite() = 0;
+};
+
+class NetCopy : public NetCopyInterface {
+public:
+    explicit NetCopy() {}
+
+    int Open(const char* filename, uint32_t arg, size_t* file_size) final {
+        return netcp_open(filename, arg, file_size);
+    }
+
+    ssize_t Read(void* data_out, std::optional<off_t> offset, size_t max_len) final {
+        if (offset) {
+            return netcp_offset_read(data_out, *offset, max_len);
+        } else {
+            return netcp_read(data_out, max_len);
+        }
+    }
+
+    ssize_t Write(const char* data, std::optional<off_t> offset, size_t length) final {
+        if (offset) {
+            return netcp_offset_write(data, *offset, length);
+        } else {
+            return netcp_write(data, length);
+        }
+    }
+
+    int Close() final {
+        return netcp_close();
+    }
+
+    void AbortWrite() final {
+        netcp_abort_write();
+    }
+};
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/netfile.cpp b/zircon/system/core/netsvc/netfile.cpp
deleted file mode 100644
index 8b8e914..0000000
--- a/zircon/system/core/netsvc/netfile.cpp
+++ /dev/null
@@ -1,220 +0,0 @@
-// Copyright 2016 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "netsvc.h"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-#include <inet6/inet6.h>
-#include <inet6/netifc.h>
-
-#include <zircon/processargs.h>
-#include <zircon/syscalls.h>
-
-#include <zircon/boot/netboot.h>
-
-#define TMP_SUFFIX ".netsvc.tmp"
-
-netfile_state netfile = {
-    .fd = -1,
-    .offset = 0,
-    .needs_rename = false,
-    .filename = {0},
-};
-
-static int netfile_mkdir(const char* filename) {
-    const char* ptr = filename[0] == '/' ? filename + 1 : filename;
-    struct stat st;
-    char tmp[1024];
-    for (;;) {
-        ptr = strchr(ptr, '/');
-        if (!ptr) {
-            return 0;
-        }
-        memcpy(tmp, filename, ptr - filename);
-        tmp[ptr - filename] = '\0';
-        ptr += 1;
-        if (stat(tmp, &st) < 0) {
-            if (errno == ENOENT) {
-                if (mkdir(tmp, 0755) < 0) {
-                    return -1;
-                }
-            } else {
-                return -1;
-            }
-        }
-    }
-}
-
-int netfile_open(const char* filename, uint32_t arg, size_t* file_size) {
-    if (netfile.fd >= 0) {
-        printf("netsvc: closing still-open '%s', replacing with '%s'\n", netfile.filename,
-               filename);
-        close(netfile.fd);
-        netfile.fd = -1;
-    }
-    size_t len = strlen(filename);
-    strlcpy(netfile.filename, filename, sizeof(netfile.filename));
-
-    struct stat st;
-again: // label here to catch filename=/path/to/new/directory/
-    if (stat(filename, &st) == 0 && S_ISDIR(st.st_mode)) {
-        errno = EISDIR;
-        goto err;
-    }
-
-    switch (arg) {
-    case O_RDONLY:
-        netfile.needs_rename = false;
-        netfile.fd = open(filename, O_RDONLY);
-        if (file_size) {
-            *file_size = st.st_size;
-        }
-        break;
-    case O_WRONLY: {
-        // If we're writing a file, actually write to "filename + TMP_SUFFIX",
-        // and rename to the final destination when we would close. This makes
-        // written files appear to atomically update.
-        if (len + strlen(TMP_SUFFIX) + 1 > PATH_MAX) {
-            errno = ENAMETOOLONG;
-            goto err;
-        }
-        strcat(netfile.filename, TMP_SUFFIX);
-        netfile.needs_rename = true;
-        netfile.fd = open(netfile.filename, O_WRONLY | O_CREAT | O_TRUNC);
-        netfile.filename[len] = '\0';
-        if (netfile.fd < 0 && errno == ENOENT) {
-            if (netfile_mkdir(filename) == 0) {
-                goto again;
-            }
-        }
-        break;
-    }
-    default:
-        printf("netsvc: open '%s' with invalid mode %d\n", filename, arg);
-        errno = EINVAL;
-    }
-    if (netfile.fd < 0) {
-        goto err;
-    } else {
-        strlcpy(netfile.filename, filename, sizeof(netfile.filename));
-        netfile.offset = 0;
-    }
-
-    return 0;
-err:
-    netfile.filename[0] = '\0';
-    return -errno;
-}
-
-ssize_t netfile_offset_read(void* data_out, off_t offset, size_t max_len) {
-    if (netfile.fd < 0) {
-        printf("netsvc: read, but no open file\n");
-        return -EBADF;
-    }
-    if (offset != netfile.offset) {
-        if (lseek(netfile.fd, offset, SEEK_SET) != offset) {
-            return -errno;
-        }
-        netfile.offset = offset;
-    }
-    return netfile_read(data_out, max_len);
-}
-
-ssize_t netfile_read(void* data_out, size_t data_sz) {
-    if (netfile.fd < 0) {
-        printf("netsvc: read, but no open file\n");
-        return -EBADF;
-    }
-    ssize_t n = read(netfile.fd, data_out, data_sz);
-    if (n < 0) {
-        printf("netsvc: error reading '%s': %d\n", netfile.filename, errno);
-        int result = (errno == 0) ? -EIO : -errno;
-        close(netfile.fd);
-        netfile.fd = -1;
-        return result;
-    }
-    netfile.offset += n;
-    return n;
-}
-
-ssize_t netfile_offset_write(const char* data, off_t offset, size_t length) {
-    if (netfile.fd < 0) {
-        printf("netsvc: write, but no open file\n");
-        return -EBADF;
-    }
-    if (offset != netfile.offset) {
-        if (lseek(netfile.fd, offset, SEEK_SET) != offset) {
-            return -errno;
-        }
-        netfile.offset = offset;
-    }
-    return netfile_write(data, length);
-}
-
-ssize_t netfile_write(const char* data, size_t len) {
-    if (netfile.fd < 0) {
-        printf("netsvc: write, but no open file\n");
-        return -EBADF;
-    }
-    ssize_t n = write(netfile.fd, data, len);
-    if (n != static_cast<ssize_t>(len)) {
-        printf("netsvc: error writing %s: %d\n", netfile.filename, errno);
-        int result = (errno == 0) ? -EIO : -errno;
-        close(netfile.fd);
-        netfile.fd = -1;
-        return result;
-    }
-    netfile.offset += len;
-    return len;
-}
-
-int netfile_close() {
-    int result = 0;
-    if (netfile.fd < 0) {
-        printf("netsvc: close, but no open file\n");
-    } else {
-        if (netfile.needs_rename) {
-            char src[PATH_MAX];
-            strlcpy(src, netfile.filename, sizeof(src));
-            strlcat(src, TMP_SUFFIX, sizeof(src));
-            if (rename(src, netfile.filename)) {
-                printf("netsvc: failed to rename temporary file: %s\n", strerror(errno));
-            }
-        }
-        if (close(netfile.fd)) {
-            result = (errno == 0) ? -EIO : -errno;
-        }
-        netfile.fd = -1;
-    }
-    return result;
-}
-
-// Clean up if we abort before finishing a write. Close out and unlink it, rather than
-// leaving an incomplete file.
-void netfile_abort_write() {
-    if (netfile.fd < 0) {
-        return;
-    }
-    close(netfile.fd);
-    netfile.fd = -1;
-    char tmp[PATH_MAX];
-    const char* filename;
-    if (netfile.needs_rename) {
-        strlcpy(tmp, netfile.filename, sizeof(tmp));
-        strlcat(tmp, TMP_SUFFIX, sizeof(tmp));
-        filename = tmp;
-    } else {
-        filename = netfile.filename;
-    }
-    if (unlink(filename) != 0) {
-        printf("netsvc: failed to unlink aborted file %s\n", filename);
-    }
-}
diff --git a/zircon/system/core/netsvc/netsvc.cpp b/zircon/system/core/netsvc/netsvc.cpp
index 3d7b470..ebc8002 100644
--- a/zircon/system/core/netsvc/netsvc.cpp
+++ b/zircon/system/core/netsvc/netsvc.cpp
@@ -23,12 +23,18 @@
 #include <zircon/time.h>
 
 #include "device_id.h"
+#include "debuglog.h"
+#include "netboot.h"
+#include "tftp.h"
 
 #define FILTER_IPV6 1
 
-bool netbootloader = false;
+static bool g_netbootloader = false;
 
-const char* nodename = "zircon";
+static const char* g_nodename = "zircon";
+
+bool netbootloader() { return g_netbootloader; }
+const char* nodename() { return g_nodename; }
 
 void udp6_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport,
                const ip6_addr_t* saddr, uint16_t sport) {
@@ -63,8 +69,9 @@
 
 void update_timeouts() {
     zx_time_t now = zx_clock_get_monotonic();
-    zx_time_t next_timeout =
-        (debuglog_next_timeout < tftp_next_timeout) ? debuglog_next_timeout : tftp_next_timeout;
+    zx_time_t next_timeout = (debuglog_next_timeout() < tftp_next_timeout())
+                                 ? debuglog_next_timeout()
+                                 : tftp_next_timeout();
     if (next_timeout != ZX_TIME_INFINITE) {
         uint32_t ms = static_cast<uint32_t>(
             (next_timeout < now) ? 0 : (zx_time_sub_time(next_timeout, now)) / ZX_MSEC(1));
@@ -96,7 +103,7 @@
     bool should_advertise = false;
     while (argc > 1) {
         if (!strncmp(argv[1], "--netboot", 9)) {
-            netbootloader = true;
+            g_netbootloader = true;
         } else if (!strncmp(argv[1], "--advertise", 11)) {
             should_advertise = true;
         } else if (!strncmp(argv[1], "--interface", 11)) {
@@ -111,7 +118,7 @@
         } else if (!strncmp(argv[1], "--nodename", 10)) {
             print_nodename_and_exit = true;
         } else {
-            nodename = argv[1];
+            g_nodename = argv[1];
             nodename_provided = true;
         }
         argv++;
@@ -131,25 +138,25 @@
         if (!nodename_provided) {
             netifc_get_info(mac, &mtu);
             device_id_get(mac, device_id);
-            nodename = device_id;
+            g_nodename = device_id;
             if (print_nodename_and_exit) {
-                printf("%s\n", nodename);
+                printf("%s\n", g_nodename);
                 return 0;
             }
         }
 
-        if (netbootloader) {
+        if (g_netbootloader) {
             printf("%szedboot: version: %s\n\n", zedboot_banner, BOOTLOADER_VERSION);
         }
 
-        printf("netsvc: nodename='%s'\n", nodename);
+        printf("netsvc: nodename='%s'\n", g_nodename);
         if (!should_advertise) {
             printf("netsvc: will not advertise\n");
         }
         printf("netsvc: start\n");
         for (;;) {
-            if (netbootloader && should_advertise) {
-                netboot_advertise(nodename);
+            if (g_netbootloader && should_advertise) {
+                netboot_advertise(g_nodename);
             }
 
             update_timeouts();
@@ -159,10 +166,10 @@
                 break;
             }
             zx_time_t now = zx_clock_get_monotonic();
-            if (now > debuglog_next_timeout) {
+            if (now > debuglog_next_timeout()) {
                 debuglog_timeout_expired();
             }
-            if (now > tftp_next_timeout) {
+            if (now > tftp_next_timeout()) {
                 tftp_timeout_expired();
             }
         }
diff --git a/zircon/system/core/netsvc/netsvc.h b/zircon/system/core/netsvc/netsvc.h
index 727f5e9..1cd48ad 100644
--- a/zircon/system/core/netsvc/netsvc.h
+++ b/zircon/system/core/netsvc/netsvc.h
@@ -4,79 +4,7 @@
 
 #pragma once
 
-#include <limits.h>
-#include <stdio.h>
-
-#include <inet6/inet6.h>
-
-#include <atomic>
-
-#include <zircon/boot/netboot.h>
-#include <zircon/types.h>
-
-// netfile interface
-struct netfile_state {
-    int fd;
-    off_t offset;
-    // false: Filename is the open file and final destination
-    // true : Filename is final destination; open file has a magic tmp suffix
-    bool needs_rename;
-    char filename[PATH_MAX];
-};
-
-extern netfile_state netfile;
-
-struct netfilemsg {
-    nbmsg hdr;
-    uint8_t data[1024];
-};
-
-int netfile_open(const char* filename, uint32_t arg, size_t* file_size);
-
-ssize_t netfile_offset_read(void* data_out, off_t offset, size_t max_len);
-
-ssize_t netfile_read(void* data_out, size_t data_sz);
-
-ssize_t netfile_offset_write(const char* data, off_t offset, size_t length);
-
-ssize_t netfile_write(const char* data, size_t len);
-
-int netfile_close();
-
-void netfile_abort_write();
-
-// netboot interface
-extern const char* nodename;
-extern bool netbootloader;
-
-void netboot_advertise(const char* nodename);
-
-void netboot_recv(void* data, size_t len, bool is_mcast, const ip6_addr_t* daddr, uint16_t dport,
-                  const ip6_addr_t* saddr, uint16_t sport);
-
-void netboot_run_cmd(const char* cmd);
-
-// TFTP interface
-extern zx_time_t tftp_next_timeout;
-extern std::atomic<bool> paving_in_progress;
-extern std::atomic<int> paver_exit_code;
-
-void tftp_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport,
-               const ip6_addr_t* saddr, uint16_t sport);
-
-void tftp_timeout_expired();
-
-bool tftp_has_pending();
-void tftp_send_next();
-
-// debuglog interface
-extern zx_time_t debuglog_next_timeout;
-
-int debuglog_init();
-
-void debuglog_recv(void* data, size_t len, bool is_mcast);
-
-void debuglog_timeout_expired();
-
-// netsvc interface
 void update_timeouts();
+
+bool netbootloader();
+const char* nodename();
diff --git a/zircon/system/core/netsvc/paver.cpp b/zircon/system/core/netsvc/paver.cpp
new file mode 100644
index 0000000..5391e88
--- /dev/null
+++ b/zircon/system/core/netsvc/paver.cpp
@@ -0,0 +1,323 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "paver.h"
+
+#include <algorithm>
+
+#include <fbl/auto_call.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/fdio/directory.h>
+#include <lib/zx/time.h>
+#include <zircon/boot/netboot.h>
+
+#include "payload-streamer.h"
+
+namespace netsvc {
+namespace {
+
+size_t NB_IMAGE_PREFIX_LEN() {
+    return strlen(NB_IMAGE_PREFIX);
+}
+size_t NB_FILENAME_PREFIX_LEN() {
+    return strlen(NB_FILENAME_PREFIX);
+}
+
+} // namespace
+
+Paver* Paver::Get() {
+    static Paver* instance_ = nullptr;
+    if (instance_ == nullptr) {
+        zx::channel local, remote;
+        auto status = zx::channel::create(0, &local, &remote);
+        if (status != ZX_OK) {
+            return nullptr;
+        }
+        status = fdio_service_connect("/svc", remote.release());
+        if (status != ZX_OK) {
+            return nullptr;
+        }
+        instance_ = new Paver(std::move(local));
+    }
+    return instance_;
+}
+
+bool Paver::InProgress() {
+    return in_progress_.load();
+}
+zx_status_t Paver::exit_code() {
+    return exit_code_.load();
+}
+void Paver::reset_exit_code() {
+    exit_code_.store(ZX_OK);
+}
+
+int Paver::StreamBuffer() {
+  zx::time last_reported = zx::clock::get_monotonic();
+    int result = 0;
+    auto callback = [this, &last_reported, &result](void* buf, size_t read_offset,
+                                                    size_t size, size_t* actual) {
+        if (read_offset >= size_) {
+            *actual = 0;
+            return ZX_OK;
+        }
+        sync_completion_reset(&data_ready_);
+        size_t write_offset = write_offset_.load();
+        while (write_offset == read_offset) {
+            // Wait for more data to be written -- we are allowed up to 3 tftp timeouts before
+            // a connection is dropped, so we should wait at least that long before giving up.
+            auto status = sync_completion_wait(&data_ready_, timeout_.get());
+            if (status != ZX_OK) {
+                printf("netsvc: timed out while waiting for data in paver-copy thread\n");
+                exit_code_.store(status);
+                result = TFTP_ERR_TIMED_OUT;
+                return ZX_ERR_TIMED_OUT;
+            }
+            sync_completion_reset(&data_ready_);
+            write_offset = write_offset_.load();
+        };
+        size = std::min(size, write_offset - read_offset);
+        memcpy(buf, buffer() + read_offset, size);
+        *actual = size;
+        zx::time curr_time = zx::clock::get_monotonic();
+        if (curr_time - last_reported >= zx::sec(1)) {
+            float complete =
+                (static_cast<float>(read_offset) / static_cast<float>(size_)) *
+                100.f;
+            printf("netsvc: paver write progress %0.1f%%\n", complete);
+            last_reported = curr_time;
+        }
+        return ZX_OK;
+    };
+
+    fbl::AutoCall cleanup([this, &result]() {
+        unsigned int refcount = std::atomic_fetch_sub(&buf_refcount_, 1u);
+        if (refcount == 1) {
+            buffer_mapper_.Reset();
+        }
+
+        paver_svc_.reset();
+
+        if (result != 0) {
+            printf("netsvc: copy exited prematurely (%d): expect paver errors\n", result);
+        }
+
+        in_progress_.store(false);
+    });
+
+    zx::channel client, server;
+    auto status = zx::channel::create(0, &client, &server);
+    if (status) {
+        fprintf(stderr, "netsvc: unable to create channel\n");
+        exit_code_.store(status);
+        return 0;
+    }
+
+    async::Loop loop(&kAsyncLoopConfigAttachToThread);
+    PayloadStreamer streamer(std::move(server), std::move(callback));
+    loop.StartThread("payload-streamer");
+
+    // Blocks untils paving is complete.
+    auto io_status = fuchsia_paver_PaverWriteVolumes(paver_svc_.get(), client.release(),
+                                                     &status);
+    status = io_status == ZX_OK ? status : io_status;
+    exit_code_.store(status);
+
+    return 0;
+}
+
+int Paver::MonitorBuffer() {
+    int result = TFTP_NO_ERROR;
+
+    fbl::AutoCall cleanup([this, &result]() {
+        unsigned int refcount = std::atomic_fetch_sub(&buf_refcount_, 1u);
+        if (refcount == 1) {
+            buffer_mapper_.Reset();
+        }
+
+        paver_svc_.reset();
+
+        if (result != 0) {
+            printf("netsvc: copy exited prematurely (%d): expect paver errors\n", result);
+        }
+
+        in_progress_.store(false);
+    });
+
+    size_t write_ndx = 0;
+    do {
+        // Wait for more data to be written -- we are allowed up to 3 tftp timeouts before
+        // a connection is dropped, so we should wait at least that long before giving up.
+        auto status = sync_completion_wait(&data_ready_, timeout_.get());
+        if (status != ZX_OK) {
+            printf("netsvc: timed out while waiting for data in paver-copy thread\n");
+            exit_code_.store(status);
+            result = TFTP_ERR_TIMED_OUT;
+            return result;
+        }
+        sync_completion_reset(&data_ready_);
+        write_ndx = write_offset_.load();
+    } while (write_ndx < size_);
+
+    zx::vmo dup;
+    auto status = buffer_mapper_.vmo().duplicate(ZX_RIGHT_SAME_RIGHTS, &dup);
+    if (status != ZX_OK) {
+        exit_code_.store(status);
+        return 0;
+    }
+
+    fuchsia_mem_Buffer buffer = {
+        .vmo = dup.release(),
+        .size = buffer_mapper_.size(),
+    };
+
+    zx_status_t io_status = ZX_ERR_INTERNAL;
+    // Blocks untils paving is complete.
+    switch (command_) {
+    case Command::kDataFile:
+        io_status = fuchsia_paver_PaverWriteDataFile(paver_svc_.get(), path_, strlen(path_),
+                                                     &buffer, &status);
+        break;
+    case Command::kBootloader:
+        io_status = fuchsia_paver_PaverWriteBootloader(paver_svc_.get(), &buffer, &status);
+        break;
+    case Command::kAsset:
+        io_status = fuchsia_paver_PaverWriteAsset(paver_svc_.get(), configuration_, asset_, &buffer,
+                                                  &status);
+        break;
+    default:
+        io_status = ZX_OK;
+        result = TFTP_ERR_INTERNAL;
+        status = ZX_ERR_INTERNAL;
+        break;
+    }
+    status = io_status == ZX_OK ? status : io_status;
+    exit_code_.store(status);
+
+    return 0;
+}
+
+tftp_status Paver::OpenWrite(const char* filename, size_t size) {
+    // Paving an image to disk.
+    if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_FVM_HOST_FILENAME)) {
+        printf("netsvc: Running FVM Paver\n");
+        command_ = Command::kFvm;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_BOOTLOADER_HOST_FILENAME)) {
+        printf("netsvc: Running BOOTLOADER Paver\n");
+        command_ = Command::kBootloader;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_ZIRCONA_HOST_FILENAME)) {
+        printf("netsvc: Running ZIRCON-A Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_A;
+        asset_ = fuchsia_paver_Asset_KERNEL;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_ZIRCONB_HOST_FILENAME)) {
+        printf("netsvc: Running ZIRCON-B Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_B;
+        asset_ = fuchsia_paver_Asset_KERNEL;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_ZIRCONR_HOST_FILENAME)) {
+        printf("netsvc: Running ZIRCON-R Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_RECOVERY;
+        asset_ = fuchsia_paver_Asset_KERNEL;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_VBMETAA_HOST_FILENAME)) {
+        printf("netsvc: Running VBMETA-A Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_A;
+        asset_ = fuchsia_paver_Asset_VERIFIED_BOOT_METADATA;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_VBMETAB_HOST_FILENAME)) {
+        printf("netsvc: Running VBMETA-B Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_B;
+        asset_ = fuchsia_paver_Asset_VERIFIED_BOOT_METADATA;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_VBMETAR_HOST_FILENAME)) {
+        printf("netsvc: Running VBMETA-R Paver\n");
+        command_ = Command::kAsset;
+        configuration_ = fuchsia_paver_Configuration_RECOVERY;
+        asset_ = fuchsia_paver_Asset_VERIFIED_BOOT_METADATA;
+    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN(), NB_SSHAUTH_HOST_FILENAME)) {
+        printf("netsvc: Installing SSH authorized_keys\n");
+        command_ = Command::kDataFile;
+        strncpy(path_, "ssh/authorized_keys", PATH_MAX);
+    } else {
+        fprintf(stderr, "netsvc: Unknown Paver\n");
+        return TFTP_ERR_IO;
+    }
+
+    auto status = buffer_mapper_.CreateAndMap(size, "paver");
+    if (status != ZX_OK) {
+        printf("netsvc: unable to allocate and map buffer\n");
+        return status;
+    }
+    fbl::AutoCall buffer_cleanup([this]() { buffer_mapper_.Reset(); });
+
+    zx::channel paver_local, paver_remote;
+    status = zx::channel::create(0, &paver_local, &paver_remote);
+    if (status != ZX_OK) {
+        fprintf(stderr, "netsvc: Unable to create channel pair.\n");
+        return TFTP_ERR_IO;
+    }
+    status = fdio_service_connect_at(svc_root_.get(), fuchsia_paver_Paver_Name,
+                                     paver_remote.release());
+    if (status != ZX_OK) {
+        fprintf(stderr, "netsvc: Unable to open /svc/%s.\n", fuchsia_paver_Paver_Name);
+        return TFTP_ERR_IO;
+    }
+
+    paver_svc_ = std::move(paver_local);
+    fbl::AutoCall svc_cleanup([&]() { paver_svc_.reset(); });
+
+    size_ = size;
+
+    buf_refcount_.store(2u);
+    write_offset_.store(0ul);
+    exit_code_.store(0);
+    in_progress_.store(true);
+
+    sync_completion_reset(&data_ready_);
+
+    auto thread_fn = command_ == Command::kFvm
+                         ? [](void* arg) { return static_cast<Paver*>(arg)->StreamBuffer(); }
+                         : [](void* arg) { return static_cast<Paver*>(arg)->MonitorBuffer(); };
+    if (thrd_create(&buf_thrd_, thread_fn, this) != thrd_success) {
+        fprintf(stderr, "netsvc: unable to launch buffer stream/monitor thread\n");
+        status = ZX_ERR_NO_RESOURCES;
+        return status;
+    }
+    thrd_detach(buf_thrd_);
+    svc_cleanup.cancel();
+    buffer_cleanup.cancel();
+
+    return TFTP_NO_ERROR;
+}
+
+tftp_status Paver::Write(const void* data, size_t* length, off_t offset) {
+    if (!InProgress()) {
+        printf("netsvc: paver exited prematurely with %d\n", exit_code());
+        reset_exit_code();
+        return TFTP_ERR_IO;
+    }
+
+    if ((static_cast<size_t>(offset) > size_) ||
+        (offset + *length) > size_) {
+        return TFTP_ERR_INVALID_ARGS;
+    }
+    memcpy(&buffer()[offset], data, *length);
+    size_t new_offset = offset + *length;
+    write_offset_.store(new_offset);
+    // Wake the paver thread, if it is waiting for data
+    sync_completion_signal(&data_ready_);
+    return TFTP_NO_ERROR;
+}
+
+void Paver::Close() {
+    unsigned int refcount = std::atomic_fetch_sub(&buf_refcount_, 1u);
+    if (refcount == 1) {
+        buffer_mapper_.Reset();
+    }
+    // TODO: Signal thread to wake up rather than wait for it to timeout if
+    // stream is closed before write is complete?
+}
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/paver.h b/zircon/system/core/netsvc/paver.h
new file mode 100644
index 0000000..979d3bb
--- /dev/null
+++ b/zircon/system/core/netsvc/paver.h
@@ -0,0 +1,112 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <atomic>
+#include <threads.h>
+
+#include <limits.h>
+
+#include <fuchsia/paver/c/fidl.h>
+#include <lib/fzl/owned-vmo-mapper.h>
+#include <lib/sync/completion.h>
+#include <lib/zx/channel.h>
+#include <lib/zx/time.h>
+#include <tftp/tftp.h>
+#include <zircon/types.h>
+
+#include "tftp.h"
+
+namespace netsvc {
+
+class PaverInterface {
+public:
+    virtual bool InProgress() = 0;
+    virtual zx_status_t exit_code() = 0;
+    virtual void reset_exit_code() = 0;
+
+    // TODO: Explore returning an object which implements write and when it goes
+    // out of scope, closes.
+    virtual tftp_status OpenWrite(const char* filename, size_t size) = 0;
+    virtual tftp_status Write(const void* data, size_t* length, off_t offset) = 0;
+    virtual void Close() = 0;
+};
+
+class Paver : public PaverInterface {
+public:
+    // Get the singleton instance.
+    static Paver* Get();
+
+    bool InProgress() final;
+    zx_status_t exit_code() final;
+    void reset_exit_code() final;
+
+    tftp_status OpenWrite(const char* filename, size_t size) final;
+    tftp_status Write(const void* data, size_t* length, off_t offset) final;
+    void Close() final;
+
+    // Visible for testing.
+    explicit Paver(zx::channel svc_root)
+        : svc_root_(std::move(svc_root)) {}
+
+    void set_timeout(zx::duration timeout) { timeout_ = timeout; }
+
+private:
+    // Refer to //zircon/system/fidl/fuchsia.paver/paver.fidl for a list of what
+    // these commands translate to.
+    enum class Command {
+        kAsset,
+        kBootloader,
+        kDataFile,
+        kFvm,
+    };
+
+    uint8_t* buffer() { return static_cast<uint8_t*>(buffer_mapper_.start()); }
+
+    // Pushes all data from the paver buffer (filled by netsvc) into the paver input VMO. When
+    // there's no data to copy, blocks on data_ready until more data is written into the buffer.
+    int StreamBuffer();
+
+    // Monitors the vmo progress, and calls into paver service once finished.
+    int MonitorBuffer();
+
+    std::atomic<bool> in_progress_ = false;
+    std::atomic<zx_status_t> exit_code_ = ZX_OK;
+
+    // Total size of file
+    size_t size_ = 0;
+
+    // Paver command to call into.
+    Command command_;
+
+    // Channel to svc.
+    zx::channel svc_root_;
+
+    // Channel to paver service.
+    zx::channel paver_svc_;
+
+    union {
+        // Only valid when command == Command::kAsset.
+        struct {
+            fuchsia_paver_Configuration configuration_;
+            fuchsia_paver_Asset asset_;
+        };
+        // Only valid when command == Command::kDataFile.
+        char path_[PATH_MAX];
+    };
+
+    // Buffer used for stashing data from tftp until it can be written out to the paver.
+    fzl::OwnedVmoMapper buffer_mapper_;
+    // Buffer write offset.
+    std::atomic<size_t> write_offset_ = 0;
+    std::atomic<unsigned int> buf_refcount_ = 0;
+    thrd_t buf_thrd_ = 0;
+    sync_completion_t data_ready_;
+
+    // Timeout monitor thread uses before timing out.
+    zx::duration timeout_ = zx::sec(5 * TFTP_TIMEOUT_SECS);
+};
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/payload-streamer.cpp b/zircon/system/core/netsvc/payload-streamer.cpp
new file mode 100644
index 0000000..1158651
--- /dev/null
+++ b/zircon/system/core/netsvc/payload-streamer.cpp
@@ -0,0 +1,63 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "payload-streamer.h"
+
+#include <lib/async/default.h>
+
+namespace netsvc {
+
+PayloadStreamer::PayloadStreamer(zx::channel chan, ReadCallback callback)
+    : read_(std::move(callback)) {
+    fidl_bind(async_get_default_dispatcher(), chan.release(),
+              reinterpret_cast<fidl_dispatch_t*>(fuchsia_paver_PayloadStream_dispatch),
+              this, &ops_);
+}
+
+zx_status_t PayloadStreamer::RegisterVmo(zx_handle_t vmo_handle, fidl_txn_t* txn) {
+    zx::vmo vmo(vmo_handle);
+
+    if (vmo_) {
+        vmo_.reset();
+        mapper_.Unmap();
+    }
+
+    vmo_ = std::move(vmo);
+    auto status = mapper_.Map(vmo_, 0, 0, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE);
+    return fuchsia_paver_PayloadStreamRegisterVmo_reply(txn, status);
+}
+
+zx_status_t PayloadStreamer::ReadData(fidl_txn_t* txn) {
+    fuchsia_paver_ReadResult result = {};
+    if (!vmo_) {
+        result.tag = fuchsia_paver_ReadResultTag_err;
+        result.err = ZX_ERR_BAD_STATE;
+        return fuchsia_paver_PayloadStreamReadData_reply(txn, &result);
+    }
+    if (eof_reached_) {
+        result.tag = fuchsia_paver_ReadResultTag_eof;
+        result.eof = true;
+        return fuchsia_paver_PayloadStreamReadData_reply(txn, &result);
+    }
+
+    size_t actual;
+    auto status = read_(mapper_.start(), read_offset_, mapper_.size(), &actual);
+    if (status != ZX_OK) {
+        result.tag = fuchsia_paver_ReadResultTag_err;
+        result.err = status;
+    } else if (actual == 0) {
+        eof_reached_ = true;
+        result.tag = fuchsia_paver_ReadResultTag_eof;
+        result.eof = true;
+    } else {
+        result.tag = fuchsia_paver_ReadResultTag_info;
+        result.info.offset = 0;
+        result.info.size = actual;
+        read_offset_ += actual;
+    }
+
+    return fuchsia_paver_PayloadStreamReadData_reply(txn, &result);
+}
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/payload-streamer.h b/zircon/system/core/netsvc/payload-streamer.h
new file mode 100644
index 0000000..98fbb7c
--- /dev/null
+++ b/zircon/system/core/netsvc/payload-streamer.h
@@ -0,0 +1,50 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <fbl/function.h>
+#include <fbl/unique_fd.h>
+#include <fuchsia/paver/c/fidl.h>
+#include <lib/fidl-utils/bind.h>
+#include <lib/fzl/vmo-mapper.h>
+#include <lib/zx/channel.h>
+#include <lib/zx/vmo.h>
+#include <zircon/types.h>
+
+namespace netsvc {
+
+// Reads the data into the vmo at offset, size. Can block.
+using ReadCallback = fbl::Function<zx_status_t(void* /*buf*/, size_t /*offset*/, size_t /*size*/,
+                                               size_t* /*actual*/)>;
+
+class PayloadStreamer {
+public:
+    PayloadStreamer(zx::channel chan, ReadCallback callback);
+
+    PayloadStreamer(const PayloadStreamer&) = delete;
+    PayloadStreamer& operator=(const PayloadStreamer&) = delete;
+    PayloadStreamer(PayloadStreamer&&) = delete;
+    PayloadStreamer& operator=(PayloadStreamer&&) = delete;
+
+    zx_status_t RegisterVmo(zx_handle_t vmo_handle, fidl_txn_t* txn);
+
+    zx_status_t ReadData(fidl_txn_t* txn);
+
+private:
+    using Binder = fidl::Binder<PayloadStreamer>;
+
+    static constexpr fuchsia_paver_PayloadStream_ops_t ops_ = {
+        .RegisterVmo = Binder::BindMember<&PayloadStreamer::RegisterVmo>,
+        .ReadData = Binder::BindMember<&PayloadStreamer::ReadData>,
+    };
+
+    ReadCallback read_;
+    zx::vmo vmo_;
+    fzl::VmoMapper mapper_;
+    size_t read_offset_ = 0;
+    bool eof_reached_ = false;
+};
+
+} // namespace netsvc
diff --git a/zircon/system/core/netsvc/test/file-api-test.cpp b/zircon/system/core/netsvc/test/file-api-test.cpp
new file mode 100644
index 0000000..0fa6541
--- /dev/null
+++ b/zircon/system/core/netsvc/test/file-api-test.cpp
@@ -0,0 +1,247 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "file-api.h"
+
+#include <memory>
+
+#include <fcntl.h>
+
+#include <fuchsia/sysinfo/c/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/fidl-utils/bind.h>
+#include <zxtest/zxtest.h>
+
+namespace {
+
+class FakePaver : public netsvc::PaverInterface {
+public:
+    bool InProgress() override { return in_progress_; }
+    zx_status_t exit_code() override { return exit_code_; }
+    void reset_exit_code() override { exit_code_ = ZX_OK; }
+
+    tftp_status OpenWrite(const char* filename, size_t size) override {
+        in_progress_ = true;
+        return TFTP_NO_ERROR;
+    }
+    tftp_status Write(const void* data, size_t* length, off_t offset) override {
+        if (!in_progress_) {
+            return TFTP_ERR_INTERNAL;
+        }
+        exit_code_ = ZX_OK;
+        return TFTP_NO_ERROR;
+    }
+    void Close() override {
+        in_progress_ = false;
+    }
+
+    void set_exit_code(zx_status_t exit_code) {
+        exit_code_ = exit_code;
+    }
+
+private:
+    bool in_progress_ = false;
+    zx_status_t exit_code_ = ZX_OK;
+};
+
+constexpr char kReadData[] = "laksdfjsadfa";
+constexpr char kFakeData[] = "lalala";
+
+class FakeNetCopy : public netsvc::NetCopyInterface {
+public:
+    int Open(const char* filename, uint32_t arg, size_t* file_size) override {
+        if (arg == O_RDONLY) {
+            *file_size = sizeof(kReadData);
+        }
+        return 0;
+    }
+    ssize_t Read(void* data_out, std::optional<off_t> offset, size_t max_len) override {
+        const size_t len = std::min(sizeof(kReadData), max_len);
+        memcpy(data_out, kReadData, len);
+        return len;
+    }
+    ssize_t Write(const char* data, std::optional<off_t> offset, size_t length) override {
+        return length;
+    }
+    int Close() override {
+        return 0;
+    }
+    void AbortWrite() override {}
+};
+
+class FakeSysinfo {
+public:
+    FakeSysinfo(async_dispatcher_t* dispatcher) {
+        zx::channel remote;
+        ASSERT_OK(zx::channel::create(0, &remote, &svc_chan_));
+        fidl_bind(dispatcher, remote.release(),
+                  reinterpret_cast<fidl_dispatch_t*>(fuchsia_sysinfo_Device_dispatch),
+                  this, &ops_);
+    }
+
+    zx_status_t GetRootJob(fidl_txn_t* txn) {
+        return fuchsia_sysinfo_DeviceGetRootJob_reply(txn, ZX_ERR_NOT_SUPPORTED, ZX_HANDLE_INVALID);
+    }
+
+    zx_status_t GetRootResource(fidl_txn_t* txn) {
+        return fuchsia_sysinfo_DeviceGetRootResource_reply(txn, ZX_ERR_NOT_SUPPORTED,
+                                                           ZX_HANDLE_INVALID);
+    }
+
+    zx_status_t GetHypervisorResource(fidl_txn_t* txn) {
+        return fuchsia_sysinfo_DeviceGetHypervisorResource_reply(txn, ZX_ERR_NOT_SUPPORTED,
+                                                                 ZX_HANDLE_INVALID);
+    }
+
+    zx_status_t GetBoardName(fidl_txn_t* txn) {
+        return fuchsia_sysinfo_DeviceGetBoardName_reply(txn, ZX_OK, board_, 32);
+    }
+
+    zx_status_t GetInterruptControllerInfo(fidl_txn_t* txn) {
+        return fuchsia_sysinfo_DeviceGetInterruptControllerInfo_reply(txn, ZX_ERR_NOT_SUPPORTED,
+                                                                      nullptr);
+    }
+
+    zx::channel& svc_chan() { return svc_chan_; }
+
+    void set_board_name(const char* board) {
+        strncpy(board_, board, 32);
+    }
+
+private:
+    using Binder = fidl::Binder<FakeSysinfo>;
+
+    zx::channel svc_chan_;
+
+    char board_[32] = {};
+
+    static constexpr fuchsia_sysinfo_Device_ops_t ops_ = {
+        .GetRootJob = Binder::BindMember<&FakeSysinfo::GetRootJob>,
+        .GetRootResource = Binder::BindMember<&FakeSysinfo::GetRootResource>,
+        .GetHypervisorResource = Binder::BindMember<&FakeSysinfo::GetHypervisorResource>,
+        .GetBoardName = Binder::BindMember<&FakeSysinfo::GetBoardName>,
+        .GetInterruptControllerInfo = Binder::BindMember<&FakeSysinfo::GetInterruptControllerInfo>,
+    };
+};
+
+} // namespace
+
+class FileApiTest : public zxtest::Test {
+protected:
+    FileApiTest()
+        : loop_(&kAsyncLoopConfigNoAttachToThread),
+          fake_sysinfo_(loop_.dispatcher()),
+          file_api_(true, std::make_unique<FakeNetCopy>(), std::move(fake_sysinfo_.svc_chan()),
+                    &fake_paver_) {
+
+        loop_.StartThread("file-api-test-loop");
+    }
+
+    async::Loop loop_;
+    FakePaver fake_paver_;
+    FakeSysinfo fake_sysinfo_;
+    netsvc::FileApi file_api_;
+};
+
+TEST_F(FileApiTest, OpenReadNetCopy) {
+    ASSERT_EQ(file_api_.OpenRead("file"), sizeof(kReadData));
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenReadFailedPave) {
+    fake_paver_.set_exit_code(ZX_ERR_INTERNAL);
+    ASSERT_NE(file_api_.OpenRead("file"), sizeof(kReadData));
+}
+
+TEST_F(FileApiTest, OpenWriteNetCopy) {
+    ASSERT_EQ(file_api_.OpenWrite("file", 10), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenWriteBoardName) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_BOARD_NAME_FILENAME, 10), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenWritePaver) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenWriteWhilePaving) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    ASSERT_NE(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenReadWhilePaving) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    ASSERT_LT(file_api_.OpenRead("file"), 0);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, OpenWriteFailedPave) {
+    fake_paver_.set_exit_code(ZX_ERR_INTERNAL);
+    ASSERT_NE(file_api_.OpenWrite("file", 10), TFTP_NO_ERROR);
+}
+
+TEST_F(FileApiTest, WriteNetCopy) {
+    ASSERT_EQ(file_api_.OpenWrite("file", 10), TFTP_NO_ERROR);
+    size_t len = sizeof(kFakeData);
+    ASSERT_EQ(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(len, sizeof(kFakeData));
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, WriteBoardName) {
+    fake_sysinfo_.set_board_name(kFakeData);
+    ASSERT_EQ(file_api_.OpenWrite(NB_BOARD_NAME_FILENAME, 10), TFTP_NO_ERROR);
+    size_t len = sizeof(kFakeData);
+    ASSERT_EQ(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(len, sizeof(kFakeData));
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, WriteWrongBoardName) {
+    fake_sysinfo_.set_board_name("other");
+    ASSERT_EQ(file_api_.OpenWrite(NB_BOARD_NAME_FILENAME, 10), TFTP_NO_ERROR);
+    size_t len = sizeof(kFakeData);
+    ASSERT_NE(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, WritePaver) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    size_t len = sizeof(kFakeData);
+    ASSERT_EQ(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(len, sizeof(kFakeData));
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, WriteAfterClose) {
+    ASSERT_EQ(file_api_.OpenWrite("file", 10), TFTP_NO_ERROR);
+    file_api_.Close();
+    size_t len = sizeof(kFakeData);
+    ASSERT_NE(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+}
+
+TEST_F(FileApiTest, WriteNoLength) {
+    ASSERT_EQ(file_api_.OpenWrite(NB_IMAGE_PREFIX, 10), TFTP_NO_ERROR);
+    ASSERT_NE(file_api_.Write(kFakeData, nullptr, 0), TFTP_NO_ERROR);
+    file_api_.Close();
+}
+
+TEST_F(FileApiTest, WriteWithoutOpen) {
+    size_t len = sizeof(kFakeData);
+    ASSERT_NE(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+}
+
+TEST_F(FileApiTest, AbortNetCopyWrite) {
+    ASSERT_EQ(file_api_.OpenWrite("file", 10), TFTP_NO_ERROR);
+    size_t len = sizeof(kFakeData);
+    ASSERT_EQ(file_api_.Write(kFakeData, &len, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(len, sizeof(kFakeData));
+    file_api_.Abort();
+}
+
diff --git a/zircon/system/core/netsvc/test/paver-test.cpp b/zircon/system/core/netsvc/test/paver-test.cpp
new file mode 100644
index 0000000..8c7eb72
--- /dev/null
+++ b/zircon/system/core/netsvc/test/paver-test.cpp
@@ -0,0 +1,400 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "paver.h"
+
+#include <algorithm>
+
+#include <fs/pseudo-dir.h>
+#include <fs/service.h>
+#include <fs/synchronous-vfs.h>
+#include <fuchsia/paver/c/fidl.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/fidl-utils/bind.h>
+#include <lib/zx/channel.h>
+#include <zircon/boot/netboot.h>
+#include <zxtest/zxtest.h>
+
+namespace {
+constexpr char kFakeData[] = "lalala";
+}
+
+TEST(PaverTest, Constructor) {
+    netsvc::Paver paver(zx::channel);
+}
+
+TEST(PaverTest, GetSingleton) {
+    ASSERT_NOT_NULL(netsvc::Paver::Get());
+}
+
+TEST(PaverTest, InitialInProgressFalse) {
+    zx::channel chan;
+    netsvc::Paver paver_(std::move(chan));
+    ASSERT_FALSE(paver_.InProgress());
+}
+
+TEST(PaverTest, InitialExitCodeValid) {
+    zx::channel chan;
+    netsvc::Paver paver_(std::move(chan));
+    ASSERT_OK(paver_.exit_code());
+}
+
+namespace {
+
+enum class Command {
+    kUnknown,
+    kQueryActiveConfiguration,
+    kSetActiveConfiguration,
+    kMarkActiveConfigurationSuccessful,
+    kForceRecoveryConfiguration,
+    kWriteAsset,
+    kWriteVolumes,
+    kWriteBootloader,
+    kWriteDataFile,
+    kWipeVolumes,
+};
+
+class FakePaver {
+public:
+    zx_status_t Connect(async_dispatcher_t* dispatcher, zx::channel request) {
+        return fidl_bind(dispatcher, request.release(),
+                         reinterpret_cast<fidl_dispatch_t*>(fuchsia_paver_Paver_dispatch),
+                         this, &ops_);
+    }
+
+    zx_status_t QueryActiveConfiguration(fidl_txn_t* txn) {
+        last_command_ = Command::kQueryActiveConfiguration;
+        return fuchsia_paver_PaverQueryActiveConfiguration_reply(txn, ZX_ERR_NOT_SUPPORTED,
+                                                                 nullptr);
+    }
+
+    zx_status_t SetActiveConfiguration(fuchsia_paver_Configuration configuration,
+                                       fidl_txn_t* txn) {
+        last_command_ = Command::kSetActiveConfiguration;
+        return fuchsia_paver_PaverSetActiveConfiguration_reply(txn, ZX_ERR_NOT_SUPPORTED);
+    }
+
+    zx_status_t MarkActiveConfigurationSuccessful(fidl_txn_t* txn) {
+        last_command_ = Command::kMarkActiveConfigurationSuccessful;
+        return fuchsia_paver_PaverMarkActiveConfigurationSuccessful_reply(txn,
+                                                                          ZX_ERR_NOT_SUPPORTED);
+    }
+
+    zx_status_t ForceRecoveryConfiguration(fidl_txn_t* txn) {
+        last_command_ = Command::kForceRecoveryConfiguration;
+        return fuchsia_paver_PaverForceRecoveryConfiguration_reply(txn, ZX_ERR_NOT_SUPPORTED);
+    }
+
+    zx_status_t WriteAsset(fuchsia_paver_Configuration configuration,
+                           fuchsia_paver_Asset asset, const fuchsia_mem_Buffer* payload,
+                           fidl_txn_t* txn) {
+        last_command_ = Command::kWriteAsset;
+        zx_handle_close(payload->vmo);
+        auto status = payload->size == expected_payload_size_ ? ZX_OK : ZX_ERR_INVALID_ARGS;
+        return fuchsia_paver_PaverWriteAsset_reply(txn, status);
+    }
+
+    zx_status_t WriteVolumes(zx_handle_t payload_stream, fidl_txn_t* txn) {
+        last_command_ = Command::kWriteVolumes;
+        zx::channel stream(payload_stream);
+        // Register VMO.
+        zx::vmo vmo;
+        auto status = zx::vmo::create(1024, 0, &vmo);
+        if (status != ZX_OK) {
+            return fuchsia_paver_PaverWriteVolumes_reply(txn, status);
+        }
+        auto io_status = fuchsia_paver_PayloadStreamRegisterVmo(stream.get(), vmo.release(),
+                                                                &status);
+        status = io_status == ZX_OK ? status : io_status;
+        if (status != ZX_OK) {
+            return fuchsia_paver_PaverWriteVolumes_reply(txn, status);
+        }
+        // Stream until EOF.
+        status = [&]() {
+            size_t data_transferred = 0;
+            for (;;) {
+                fuchsia_paver_ReadResult result;
+                auto io_status = fuchsia_paver_PayloadStreamReadData(stream.get(), &result);
+                if (io_status != ZX_OK) {
+                    return io_status;
+                }
+                switch (result.tag) {
+                case fuchsia_paver_ReadResultTag_err:
+                    return result.err;
+                case fuchsia_paver_ReadResultTag_eof:
+                    return data_transferred == expected_payload_size_ ? ZX_OK : ZX_ERR_INVALID_ARGS;
+                case fuchsia_paver_ReadResultTag_info:
+                    data_transferred += result.info.size;
+                    continue;
+                default:
+                    return ZX_ERR_INTERNAL;
+                }
+            }
+        }();
+
+        return fuchsia_paver_PaverWriteVolumes_reply(txn, status);
+    }
+
+    zx_status_t WriteBootloader(const fuchsia_mem_Buffer* payload, fidl_txn_t* txn) {
+        last_command_ = Command::kWriteBootloader;
+        zx_handle_close(payload->vmo);
+        auto status = payload->size == expected_payload_size_ ? ZX_OK : ZX_ERR_INVALID_ARGS;
+        return fuchsia_paver_PaverWriteBootloader_reply(txn, status);
+    }
+
+    zx_status_t WriteDataFile(const char* filename, size_t filename_len,
+                              const fuchsia_mem_Buffer* payload, fidl_txn_t* txn) {
+        last_command_ = Command::kWriteDataFile;
+        zx_handle_close(payload->vmo);
+        auto status = payload->size == expected_payload_size_ ? ZX_OK : ZX_ERR_INVALID_ARGS;
+        return fuchsia_paver_PaverWriteDataFile_reply(txn, status);
+    }
+
+    zx_status_t WipeVolumes(fidl_txn_t* txn) {
+        last_command_ = Command::kWipeVolumes;
+        auto status = ZX_OK;
+        return fuchsia_paver_PaverWipeVolumes_reply(txn, status);
+    }
+
+    Command last_command() { return last_command_; }
+    void set_expected_payload_size(size_t size) { expected_payload_size_ = size; }
+
+private:
+    using Binder = fidl::Binder<FakePaver>;
+
+    Command last_command_ = Command::kUnknown;
+    size_t expected_payload_size_ = 0;
+
+    static constexpr fuchsia_paver_Paver_ops_t ops_ = {
+        .QueryActiveConfiguration = Binder::BindMember<&FakePaver::QueryActiveConfiguration>,
+        .SetActiveConfiguration = Binder::BindMember<&FakePaver::SetActiveConfiguration>,
+        .MarkActiveConfigurationSuccessful =
+            Binder::BindMember<&FakePaver::MarkActiveConfigurationSuccessful>,
+        .ForceRecoveryConfiguration = Binder::BindMember<&FakePaver::ForceRecoveryConfiguration>,
+        .WriteAsset = Binder::BindMember<&FakePaver::WriteAsset>,
+        .WriteVolumes = Binder::BindMember<&FakePaver::WriteVolumes>,
+        .WriteBootloader = Binder::BindMember<&FakePaver::WriteBootloader>,
+        .WriteDataFile = Binder::BindMember<&FakePaver::WriteDataFile>,
+        .WipeVolumes = Binder::BindMember<&FakePaver::WipeVolumes>,
+    };
+};
+
+class FakeSvc {
+public:
+    explicit FakeSvc(async_dispatcher_t* dispatcher)
+        : dispatcher_(dispatcher), vfs_(dispatcher) {
+        auto root_dir = fbl::MakeRefCounted<fs::PseudoDir>();
+        root_dir->AddEntry(fuchsia_paver_Paver_Name,
+                           fbl::MakeRefCounted<fs::Service>([this](zx::channel request) {
+                               return fake_paver_.Connect(dispatcher_, std::move(request));
+                           }));
+
+        zx::channel svc_remote;
+        ASSERT_OK(zx::channel::create(0, &svc_local_, &svc_remote));
+
+        vfs_.ServeDirectory(root_dir, std::move(svc_remote));
+    }
+
+    FakePaver& fake_paver() { return fake_paver_; }
+    zx::channel& svc_chan() { return svc_local_; }
+
+private:
+    async_dispatcher_t* dispatcher_;
+    fs::SynchronousVfs vfs_;
+    FakePaver fake_paver_;
+    zx::channel svc_local_;
+};
+
+} // namespace
+
+class PaverTest : public zxtest::Test {
+protected:
+    PaverTest()
+        : loop_(&kAsyncLoopConfigNoAttachToThread),
+          fake_svc_(loop_.dispatcher()),
+          paver_(std::move(fake_svc_.svc_chan())) {
+
+        paver_.set_timeout(zx::msec(500));
+        loop_.StartThread("paver-test-loop");
+    }
+
+    ~PaverTest() {
+        // Need to make sure paver thread exits.
+        Wait();
+        loop_.Shutdown();
+    }
+
+    void Wait() {
+        while (paver_.InProgress())
+            continue;
+    }
+
+    async::Loop loop_;
+    FakeSvc fake_svc_;
+    netsvc::Paver paver_;
+};
+
+TEST_F(PaverTest, OpenWriteInvalidFile) {
+    char invalid_file_name[32] = {};
+    ASSERT_NE(paver_.OpenWrite(invalid_file_name, 0), TFTP_NO_ERROR);
+    paver_.Close();
+}
+
+TEST_F(PaverTest, OpenWriteInvalidSize) {
+    ASSERT_NE(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 0), TFTP_NO_ERROR);
+}
+
+TEST_F(PaverTest, OpenWriteValidFile) {
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    paver_.Close();
+}
+
+TEST_F(PaverTest, OpenTwice) {
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    ASSERT_NE(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    paver_.Close();
+}
+
+TEST_F(PaverTest, WriteWithoutOpen) {
+    size_t size = sizeof(kFakeData);
+    ASSERT_NE(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+}
+
+TEST_F(PaverTest, WriteAfterClose) {
+    size_t size = sizeof(kFakeData);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    paver_.Close();
+    // TODO(surajmalhotra): Should we ensure this fails?
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+}
+
+TEST_F(PaverTest, TimeoutNoWrites) {
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    paver_.Close();
+    Wait();
+    ASSERT_NE(paver_.exit_code(), ZX_OK);
+}
+
+TEST_F(PaverTest, TimeoutPartialWrite) {
+    size_t size = sizeof(kFakeData);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_NE(paver_.exit_code(), ZX_OK);
+}
+
+TEST_F(PaverTest, WriteCompleteSingle) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(size);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_OK);
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteBootloader);
+}
+
+TEST_F(PaverTest, WriteCompleteManySmallWrites) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(1024);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 1024), TFTP_NO_ERROR);
+    for (size_t offset = 0; offset < 1024; offset += sizeof(kFakeData)) {
+        size = std::min(sizeof(kFakeData), 1024 - offset);
+        ASSERT_EQ(paver_.Write(kFakeData, &size, offset), TFTP_NO_ERROR);
+        ASSERT_EQ(size, std::min(sizeof(kFakeData), 1024 - offset));
+    }
+    paver_.Close();
+    Wait();
+    ASSERT_OK(paver_.exit_code());
+}
+
+TEST_F(PaverTest, Overwrite) {
+    size_t size = sizeof(kFakeData);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 2), TFTP_NO_ERROR);
+    ASSERT_NE(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    paver_.Close();
+    Wait();
+    ASSERT_NE(paver_.exit_code(), ZX_OK);
+}
+
+TEST_F(PaverTest, CloseChannelBetweenWrites) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(2 * size);
+    ASSERT_EQ(paver_.OpenWrite(NB_BOOTLOADER_FILENAME, 2 * size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    loop_.Shutdown();
+    ASSERT_EQ(paver_.Write(kFakeData, &size, size), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_ERR_PEER_CLOSED);
+}
+
+TEST_F(PaverTest, WriteZirconA) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(size);
+    ASSERT_EQ(paver_.OpenWrite(NB_ZIRCONA_FILENAME, size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_OK);
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteAsset);
+}
+
+TEST_F(PaverTest, WriteVbMetaA) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(size);
+    ASSERT_EQ(paver_.OpenWrite(NB_VBMETAA_FILENAME, size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_OK);
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteAsset);
+}
+
+TEST_F(PaverTest, WriteSshAuth) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(size);
+    ASSERT_EQ(paver_.OpenWrite(NB_SSHAUTH_FILENAME, size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_OK);
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteDataFile);
+}
+
+TEST_F(PaverTest, WriteFvm) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(size);
+    ASSERT_EQ(paver_.OpenWrite(NB_FVM_FILENAME, size), TFTP_NO_ERROR);
+    ASSERT_EQ(paver_.Write(kFakeData, &size, 0), TFTP_NO_ERROR);
+    ASSERT_EQ(size, sizeof(kFakeData));
+    paver_.Close();
+    Wait();
+    ASSERT_EQ(paver_.exit_code(), ZX_OK);
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteVolumes);
+}
+
+TEST_F(PaverTest, WriteFvmManySmallWrites) {
+    size_t size = sizeof(kFakeData);
+    fake_svc_.fake_paver().set_expected_payload_size(1024);
+    ASSERT_EQ(paver_.OpenWrite(NB_FVM_FILENAME, 1024), TFTP_NO_ERROR);
+    for (size_t offset = 0; offset < 1024; offset += sizeof(kFakeData)) {
+        size = std::min(sizeof(kFakeData), 1024 - offset);
+        ASSERT_EQ(paver_.Write(kFakeData, &size, offset), TFTP_NO_ERROR);
+        ASSERT_EQ(size, std::min(sizeof(kFakeData), 1024 - offset));
+    }
+    paver_.Close();
+    Wait();
+    ASSERT_OK(paver_.exit_code());
+    ASSERT_EQ(fake_svc_.fake_paver().last_command(), Command::kWriteVolumes);
+}
diff --git a/zircon/system/core/netsvc/test/payload-streamer-test.cpp b/zircon/system/core/netsvc/test/payload-streamer-test.cpp
new file mode 100644
index 0000000..634fba2
--- /dev/null
+++ b/zircon/system/core/netsvc/test/payload-streamer-test.cpp
@@ -0,0 +1,144 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "payload-streamer.h"
+
+#include <optional>
+
+#include <lib/async-loop/cpp/loop.h>
+#include <zxtest/zxtest.h>
+
+class PayloadStreamerTest : public zxtest::Test {
+protected:
+    PayloadStreamerTest()
+        : loop_(&kAsyncLoopConfigAttachToThread) {
+
+    }
+
+    static zx_status_t DefaultCallback(void* buf, size_t offset, size_t size, size_t* actual) {
+        *actual = size;
+        return ZX_OK;
+    }
+
+    void StartStreamer(netsvc::ReadCallback callback = DefaultCallback) {
+        zx::channel server;
+        ASSERT_OK(zx::channel::create(0, &client_, &server));
+
+        payload_streamer_.emplace(std::move(server), std::move(callback));
+        loop_.StartThread("payload-streamer-test-loop");
+    }
+
+    async::Loop loop_;
+    zx::channel client_;
+    std::optional<netsvc::PayloadStreamer> payload_streamer_;
+};
+
+TEST_F(PayloadStreamerTest, RegisterVmo) {
+    StartStreamer();
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+}
+
+TEST_F(PayloadStreamerTest, RegisterVmoTwice) {
+    StartStreamer();
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+}
+
+TEST_F(PayloadStreamerTest, ReadData) {
+    StartStreamer();
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+
+    fuchsia_paver_ReadResult result;
+    ASSERT_OK(fuchsia_paver_PayloadStreamReadData(client_.get(), &result));
+    ASSERT_EQ(result.tag, fuchsia_paver_ReadResultTag_info);
+    ASSERT_EQ(result.info.offset, 0);
+    ASSERT_EQ(result.info.size, ZX_PAGE_SIZE);
+}
+
+TEST_F(PayloadStreamerTest, ReadDataWithoutRegisterVmo) {
+    StartStreamer();
+
+    fuchsia_paver_ReadResult result;
+    ASSERT_OK(fuchsia_paver_PayloadStreamReadData(client_.get(), &result));
+    ASSERT_EQ(result.tag, fuchsia_paver_ReadResultTag_err);
+    ASSERT_NE(result.err, ZX_OK);
+}
+
+TEST_F(PayloadStreamerTest, ReadDataHalfFull) {
+    StartStreamer([](void* buf, size_t offset, size_t size, size_t* actual) {
+        *actual = size / 2;
+        return ZX_OK;
+    });
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+
+    fuchsia_paver_ReadResult result;
+    ASSERT_OK(fuchsia_paver_PayloadStreamReadData(client_.get(), &result));
+    ASSERT_EQ(result.tag, fuchsia_paver_ReadResultTag_info);
+    ASSERT_EQ(result.info.offset, 0);
+    ASSERT_EQ(result.info.size, ZX_PAGE_SIZE / 2);
+}
+
+TEST_F(PayloadStreamerTest, ReadEof) {
+    StartStreamer([](void* buf, size_t offset, size_t size, size_t* actual) {
+        *actual = 0;
+        return ZX_OK;
+    });
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+
+    fuchsia_paver_ReadResult result;
+    ASSERT_OK(fuchsia_paver_PayloadStreamReadData(client_.get(), &result));
+    ASSERT_EQ(result.tag, fuchsia_paver_ReadResultTag_eof);
+}
+
+TEST_F(PayloadStreamerTest, ReadFailure) {
+    StartStreamer([](void* buf, size_t offset, size_t size, size_t* actual) {
+        return ZX_ERR_INTERNAL;
+    });
+
+    zx::vmo vmo;
+    ASSERT_OK(zx::vmo::create(ZX_PAGE_SIZE, 0, &vmo));
+
+    zx_status_t status;
+    ASSERT_OK(fuchsia_paver_PayloadStreamRegisterVmo(client_.get(), vmo.release(), &status));
+    ASSERT_OK(status);
+
+    fuchsia_paver_ReadResult result;
+    ASSERT_OK(fuchsia_paver_PayloadStreamReadData(client_.get(), &result));
+    ASSERT_EQ(result.tag, fuchsia_paver_ReadResultTag_err);
+    ASSERT_NE(result.err, ZX_OK);
+}
diff --git a/zircon/system/core/netsvc/test/tftp-test.cpp b/zircon/system/core/netsvc/test/tftp-test.cpp
new file mode 100644
index 0000000..e17006c
--- /dev/null
+++ b/zircon/system/core/netsvc/test/tftp-test.cpp
@@ -0,0 +1,64 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "tftp.h"
+
+#include <inet6/netifc.h>
+#include <zxtest/zxtest.h>
+
+#include "file-api.h"
+
+void update_timeouts() {}
+bool netbootloader() { return false; }
+const char* nodename() { return "test"; }
+void netboot_run_cmd(const char* cmd) {}
+
+void udp6_recv(void* data, size_t len,
+               const ip6_addr_t* daddr, uint16_t dport,
+               const ip6_addr_t* saddr, uint16_t sport) {}
+
+void netifc_recv(void* data, size_t len) {}
+bool netifc_send_pending() { return false; }
+
+namespace {
+
+class FakeFileApi : public netsvc::FileApiInterface {
+public:
+    ssize_t OpenRead(const char* filename) override { return 10; }
+    tftp_status OpenWrite(const char* filename, size_t size) override { return ZX_OK; }
+    tftp_status Read(void* data, size_t* length, off_t offset) override { return ZX_OK; }
+    tftp_status Write(const void* data, size_t* length, off_t offset) override { return ZX_OK; }
+    void Close() override {}
+    void Abort() override {}
+
+    bool is_write() override { return false; }
+    const char* filename() override { return "filename"; }
+};
+
+} // namespace
+
+extern netsvc::FileApiInterface* g_file_api;
+
+class TftpTest : public zxtest::Test {
+protected:
+    TftpTest() {
+        g_file_api = &fake_file_api_;
+    }
+
+    ~TftpTest() {
+        g_file_api = nullptr;
+    }
+
+    FakeFileApi fake_file_api_;
+};
+
+TEST_F(TftpTest, NextTimeout) {
+    ASSERT_EQ(tftp_next_timeout(), ZX_TIME_INFINITE);
+}
+
+TEST_F(TftpTest, HasPending) {
+    ASSERT_FALSE(tftp_has_pending());
+}
+
+// TODO(surajmalhotra): Synthesize some tftp packets for additional tests.
diff --git a/zircon/system/core/netsvc/tftp.cpp b/zircon/system/core/netsvc/tftp.cpp
index d46a6bc..4bafc4d 100644
--- a/zircon/system/core/netsvc/tftp.cpp
+++ b/zircon/system/core/netsvc/tftp.cpp
@@ -2,19 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include <errno.h>
-#include <fcntl.h>
+#include "tftp.h"
+
 #include <stddef.h>
 #include <stdio.h>
 #include <string.h>
-#include <threads.h>
 #include <unistd.h>
 
-#include <atomic>
-
 #include <inet6/inet6.h>
 #include <lib/fdio/spawn.h>
-#include <lib/sync/completion.h>
 #include <tftp/tftp.h>
 #include <zircon/assert.h>
 #include <zircon/boot/netboot.h>
@@ -23,48 +19,17 @@
 #include <zircon/syscalls.h>
 #include <zircon/time.h>
 
-#include "board-name.h"
+#include "file-api.h"
 #include "netsvc.h"
 
 #define SCRATCHSZ 2048
 
-#define TFTP_TIMEOUT_SECS 1
+extern bool xfer_active;
 
-#define NB_IMAGE_PREFIX_LEN (strlen(NB_IMAGE_PREFIX))
-#define NB_FILENAME_PREFIX_LEN (strlen(NB_FILENAME_PREFIX))
+// Visible for test injection.
+netsvc::FileApiInterface* g_file_api = nullptr;
 
-// Identifies what the file being streamed over TFTP should be
-// used for.
-enum netfile_type_t {
-    netboot, // A bootfs file
-    paver,   // A disk image which should be paved to disk
-    board_name,  // A file containing the board name.
-                 // Expected to return error if it doesn't match the current board name.
-};
-
-struct file_info_t {
-    bool is_write;
-    char filename[PATH_MAX + 1];
-    netfile_type_t type;
-
-    // Only valid when type == netfile_type_t::netboot.
-    nbfile* netboot_file;
-
-    // Only valid when type == netfile_type_t::paver.
-    struct {
-        int fd;      // Pipe to paver process
-        size_t size; // Total size of file
-        zx_handle_t process;
-
-        // Buffer used for stashing data from tftp until it can be written out to the paver
-        zx_handle_t buffer_handle;
-        uint8_t* buffer;
-        std::atomic<unsigned int> buf_refcount;
-        std::atomic<size_t> offset; // Buffer write offset (read offset is stored locally)
-        thrd_t buf_copy_thrd;
-        sync_completion_t data_ready; // Allows read thread to block on buffer writes
-    } paver;
-};
+namespace {
 
 struct transport_info_t {
     ip6_addr_t dest_addr;
@@ -72,385 +37,41 @@
     uint32_t timeout_ms;
 };
 
-static char tftp_session_scratch[SCRATCHSZ];
+char tftp_session_scratch[SCRATCHSZ];
 char tftp_out_scratch[SCRATCHSZ];
 
-static size_t last_msg_size = 0;
-static tftp_session* session = NULL;
-static file_info_t file_info;
-static transport_info_t transport_info;
+size_t last_msg_size = 0;
+tftp_session* session = nullptr;
+transport_info_t transport_info;
 
-std::atomic<bool> paving_in_progress = false;
-std::atomic<int> paver_exit_code = 0;
-zx_time_t tftp_next_timeout = ZX_TIME_INFINITE;
+zx_time_t g_tftp_next_timeout = ZX_TIME_INFINITE;
 
-static ssize_t file_open_read(const char* filename, void* cookie) {
-    // Make sure all in-progress paving options have completed
-    if (atomic_load(&paving_in_progress) == true) {
-        return TFTP_ERR_SHOULD_WAIT;
-    }
-    if (std::atomic_load(&paver_exit_code) != 0) {
-        printf("paver exited with error: %d\n", std::atomic_load(&paver_exit_code));
-        std::atomic_store(&paver_exit_code, 0);
-        return TFTP_ERR_IO;
-    }
-    file_info_t* file_info = reinterpret_cast<file_info_t*>(cookie);
-    file_info->is_write = false;
-    strncpy(file_info->filename, filename, PATH_MAX);
-    file_info->filename[PATH_MAX] = '\0';
-    file_info->netboot_file = NULL;
-    size_t file_size;
-    if (netfile_open(filename, O_RDONLY, &file_size) == 0) {
-        return static_cast<ssize_t>(file_size);
-    }
-    return TFTP_ERR_NOT_FOUND;
+ssize_t file_open_read(const char* filename, void* cookie) {
+    auto* file_api = reinterpret_cast<netsvc::FileApiInterface*>(cookie);
+    return file_api->OpenRead(filename);
 }
 
-static zx_status_t alloc_paver_buffer(file_info_t* file_info, size_t size) {
-    zx_status_t status;
-    status = zx_vmo_create(size, 0, &file_info->paver.buffer_handle);
-    if (status != ZX_OK) {
-        printf("netsvc: unable to allocate buffer VMO\n");
-        return status;
-    }
-    zx_object_set_property(file_info->paver.buffer_handle, ZX_PROP_NAME, "paver", 5);
-    uintptr_t buffer;
-    status = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, 0,
-                         file_info->paver.buffer_handle, 0, size, &buffer);
-    if (status != ZX_OK) {
-        printf("netsvc: unable to map buffer\n");
-        zx_handle_close(file_info->paver.buffer_handle);
-        return status;
-    }
-    file_info->paver.buffer = reinterpret_cast<uint8_t*>(buffer);
-    return ZX_OK;
+tftp_status file_open_write(const char* filename, size_t size, void* cookie) {
+    auto* file_api = reinterpret_cast<netsvc::FileApiInterface*>(cookie);
+    return file_api->OpenWrite(filename, size);
 }
 
-static zx_status_t dealloc_paver_buffer(file_info_t* file_info) {
-    zx_status_t status =
-        zx_vmar_unmap(zx_vmar_root_self(), reinterpret_cast<uintptr_t>(file_info->paver.buffer),
-                      file_info->paver.size);
-    if (status != ZX_OK) {
-        printf("netsvc: failed to unmap paver buffer: %s\n", zx_status_get_string(status));
-        goto done;
-    }
-
-    status = zx_handle_close(file_info->paver.buffer_handle);
-    if (status != ZX_OK) {
-        printf("netsvc: failed to close paver buffer handle: %s\n", zx_status_get_string(status));
-    }
-
-done:
-    file_info->paver.buffer = NULL;
-    return status;
+tftp_status file_read(void* data, size_t* length, off_t offset, void* cookie) {
+    auto* file_api = reinterpret_cast<netsvc::FileApiInterface*>(cookie);
+    return file_api->Read(data, length, offset);
 }
 
-static int drain_pipe(void* arg) {
-    char buf[4096];
-    int fd = static_cast<int>(reinterpret_cast<intptr_t>(arg));
-
-    ssize_t sz;
-    while ((sz = read(fd, buf, sizeof(buf) - 1)) > 0) {
-        // ensure null termination
-        buf[sz] = '\0';
-        printf("%s", buf);
-    }
-
-    close(fd);
-    return static_cast<int>(sz);
+tftp_status file_write(const void* data, size_t* length, off_t offset, void* cookie) {
+    auto* file_api = reinterpret_cast<netsvc::FileApiInterface*>(cookie);
+    return file_api->Write(data, length, offset);
 }
 
-// Pushes all data from the paver buffer (filled by netsvc) into the paver input pipe. When
-// there's no data to copy, blocks on data_ready until more data is written into the buffer.
-static int paver_copy_buffer(void* arg) {
-    file_info_t* file_info = reinterpret_cast<file_info_t*>(arg);
-    size_t read_ndx = 0;
-    int result = 0;
-    zx_time_t last_reported = zx_clock_get_monotonic();
-    while (read_ndx < file_info->paver.size) {
-        sync_completion_reset(&file_info->paver.data_ready);
-        size_t write_ndx = atomic_load(&file_info->paver.offset);
-        if (write_ndx == read_ndx) {
-            // Wait for more data to be written -- we are allowed up to 3 tftp timeouts before
-            // a connection is dropped, so we should wait at least that long before giving up.
-            if (sync_completion_wait(&file_info->paver.data_ready, ZX_SEC(5 * TFTP_TIMEOUT_SECS)) ==
-                ZX_OK) {
-                continue;
-            }
-            printf("netsvc: timed out while waiting for data in paver-copy thread\n");
-            result = TFTP_ERR_TIMED_OUT;
-            goto done;
-        }
-        while (read_ndx < write_ndx) {
-            ssize_t r = write(file_info->paver.fd, &file_info->paver.buffer[read_ndx],
-                              write_ndx - read_ndx);
-            if (r <= 0) {
-                printf("netsvc: couldn't write to paver fd: %ld\n", r);
-                result = TFTP_ERR_IO;
-                goto done;
-            }
-            read_ndx += r;
-            zx_time_t curr_time = zx_clock_get_monotonic();
-            if (zx_time_sub_time(curr_time, last_reported) >= ZX_SEC(1)) {
-                float complete =
-                    (static_cast<float>(read_ndx) / static_cast<float>(file_info->paver.size)) *
-                    100.f;
-                printf("netsvc: paver write progress %0.1f%%\n", complete);
-                last_reported = curr_time;
-            }
-        }
-    }
-done:
-    close(file_info->paver.fd);
-
-    unsigned int refcount = std::atomic_fetch_sub(&file_info->paver.buf_refcount, 1u);
-    if (refcount == 1) {
-        dealloc_paver_buffer(file_info);
-    }
-
-    // wait for the paver to complete, as executing the paver concurrently has
-    // undefined behavior.
-    zx_signals_t signals;
-    zx_object_wait_one(file_info->paver.process, ZX_TASK_TERMINATED, zx_deadline_after(ZX_SEC(10)),
-                       &signals);
-
-    zx_info_process_t proc_info;
-    zx_object_get_info(file_info->paver.process, ZX_INFO_PROCESS, &proc_info, sizeof(proc_info),
-                       NULL, NULL);
-
-    std::atomic_store(&paver_exit_code, static_cast<int>(proc_info.return_code));
-    zx_handle_close(file_info->paver.process);
-
-    if (result != 0) {
-        printf("netsvc: copy exited prematurely (%d): expect paver errors\n", result);
-    }
-
-    // Extra protection against double-close.
-    file_info->filename[0] = '\0';
-    atomic_store(&paving_in_progress, false);
-    return result;
+void file_close(void* cookie) {
+    auto* file_api = reinterpret_cast<netsvc::FileApiInterface*>(cookie);
+    return file_api->Close();
 }
 
-static tftp_status paver_open_write(const char* filename, size_t size, file_info_t* file_info) {
-    // paving an image to disk
-    const char* argv[] = {"/boot/bin/install-disk-image", NULL, NULL, NULL, NULL};
-
-    if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_BOARD_NAME_HOST_FILENAME)) {
-        printf("netsvc: Running board name validation\n");
-        file_info->type = board_name;
-        return TFTP_NO_ERROR;
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_FVM_HOST_FILENAME)) {
-        printf("netsvc: Running FVM Paver\n");
-        argv[1] = "install-fvm";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_BOOTLOADER_HOST_FILENAME)) {
-        printf("netsvc: Running BOOTLOADER Paver\n");
-        argv[1] = "install-bootloader";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_ZIRCONA_HOST_FILENAME)) {
-        printf("netsvc: Running ZIRCON-A Paver\n");
-        argv[1] = "install-zircona";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_ZIRCONB_HOST_FILENAME)) {
-        printf("netsvc: Running ZIRCON-B Paver\n");
-        argv[1] = "install-zirconb";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_ZIRCONR_HOST_FILENAME)) {
-        printf("netsvc: Running ZIRCON-R Paver\n");
-        argv[1] = "install-zirconr";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_VBMETAA_HOST_FILENAME)) {
-        printf("netsvc: Running VBMETA-A Paver\n");
-        argv[1] = "install-vbmetaa";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_VBMETAB_HOST_FILENAME)) {
-        printf("netsvc: Running VBMETA-B Paver\n");
-        argv[1] = "install-vbmetab";
-    } else if (!strcmp(filename + NB_IMAGE_PREFIX_LEN, NB_SSHAUTH_HOST_FILENAME)) {
-        printf("netsvc: Installing SSH authorized_keys\n");
-        argv[1] = "install-data-file";
-        argv[2] = "--path";
-        argv[3] = "ssh/authorized_keys";
-    } else {
-        fprintf(stderr, "netsvc: Unknown Paver\n");
-        return TFTP_ERR_IO;
-    }
-
-    int fds[2];
-    if (pipe(fds)) {
-        return TFTP_ERR_IO;
-    }
-
-    int logfds[2];
-    if (pipe(logfds)) {
-        close(fds[0]);
-        close(fds[1]);
-        return TFTP_ERR_IO;
-    }
-
-    fdio_spawn_action_t actions[] = {
-        {.action = FDIO_SPAWN_ACTION_SET_NAME, .name = {.data = "paver"}},
-        {.action = FDIO_SPAWN_ACTION_TRANSFER_FD,
-         .fd = {.local_fd = fds[0], .target_fd = STDIN_FILENO}},
-        {.action = FDIO_SPAWN_ACTION_TRANSFER_FD,
-         .fd = {.local_fd = logfds[1], .target_fd = STDERR_FILENO}},
-    };
-
-    zx_status_t status =
-        fdio_spawn_etc(ZX_HANDLE_INVALID, FDIO_SPAWN_CLONE_ALL, argv[0], argv, NULL,
-                       countof(actions), actions, &file_info->paver.process, NULL);
-
-    if (status != ZX_OK) {
-        printf("netsvc: tftp couldn't launch paver\n");
-        goto err_close_fds;
-    }
-
-    thrd_t log_thrd;
-    if ((thrd_create(&log_thrd, drain_pipe,
-                     reinterpret_cast<void*>(static_cast<uintptr_t>(logfds[0])))) == thrd_success) {
-        thrd_detach(log_thrd);
-    } else {
-        printf("netsvc: couldn't create paver log message redirection thread\n");
-        goto err_close_fds;
-    }
-
-    if ((status = alloc_paver_buffer(file_info, size)) != ZX_OK) {
-        goto err_close_fds;
-    }
-
-    file_info->type = paver;
-    file_info->paver.fd = fds[1];
-    file_info->paver.size = size;
-    // Both the netsvc thread and the paver copy thread access the buffer, and either
-    // may be done with it first so we use a refcount to decide when to deallocate it
-    std::atomic_store(&file_info->paver.buf_refcount, 2u);
-    std::atomic_store(&file_info->paver.offset, 0ul);
-    std::atomic_store(&paver_exit_code, 0);
-    std::atomic_store(&paving_in_progress, true);
-
-    if ((thrd_create(&file_info->paver.buf_copy_thrd, paver_copy_buffer,
-                     reinterpret_cast<void*>(file_info))) != thrd_success) {
-        printf("netsvc: unable to launch buffer copy thread\n");
-        status = ZX_ERR_NO_RESOURCES;
-        goto dealloc_buffer;
-    }
-    thrd_detach(file_info->paver.buf_copy_thrd);
-
-    return TFTP_NO_ERROR;
-
-dealloc_buffer:
-    dealloc_paver_buffer(file_info);
-
-err_close_fds:
-    close(fds[1]);
-    close(logfds[0]);
-    return status;
-}
-
-static tftp_status file_open_write(const char* filename, size_t size, void* cookie) {
-    // Make sure all in-progress paving options have completed
-    if (atomic_load(&paving_in_progress) == true) {
-        return TFTP_ERR_SHOULD_WAIT;
-    }
-    if (atomic_load(&paver_exit_code) != 0) {
-        atomic_store(&paver_exit_code, 0);
-        return TFTP_ERR_IO;
-    }
-
-    file_info_t* file_info = reinterpret_cast<file_info_t*>(cookie);
-    file_info->is_write = true;
-    strncpy(file_info->filename, filename, PATH_MAX);
-    file_info->filename[PATH_MAX] = '\0';
-
-    if (netbootloader && !strncmp(filename, NB_FILENAME_PREFIX, NB_FILENAME_PREFIX_LEN)) {
-        // netboot
-        file_info->type = netboot;
-        file_info->netboot_file = netboot_get_buffer(filename, size);
-        if (file_info->netboot_file != NULL) {
-            return TFTP_NO_ERROR;
-        }
-    } else if (netbootloader & !strncmp(filename, NB_IMAGE_PREFIX, NB_IMAGE_PREFIX_LEN)) {
-        // paver
-        tftp_status status = paver_open_write(filename, size, file_info);
-        if (status != TFTP_NO_ERROR) {
-            file_info->filename[0] = '\0';
-        }
-        return status;
-    } else {
-        // netcp
-        if (netfile_open(filename, O_WRONLY, NULL) == 0) {
-            return TFTP_NO_ERROR;
-        }
-    }
-    return TFTP_ERR_INVALID_ARGS;
-}
-
-static tftp_status file_read(void* data, size_t* length, off_t offset, void* cookie) {
-    if (length == NULL) {
-        return TFTP_ERR_INVALID_ARGS;
-    }
-    ssize_t read_len = netfile_offset_read(data, offset, *length);
-    if (read_len < 0) {
-        return TFTP_ERR_IO;
-    }
-    *length = static_cast<size_t>(read_len);
-    return TFTP_NO_ERROR;
-}
-
-static tftp_status file_write(const void* data, size_t* length, off_t offset, void* cookie) {
-    if (length == NULL) {
-        return TFTP_ERR_INVALID_ARGS;
-    }
-    file_info_t* file_info = reinterpret_cast<file_info_t*>(cookie);
-    if (file_info->type == netboot && file_info->netboot_file != NULL) {
-        nbfile* nb_file = file_info->netboot_file;
-        if ((static_cast<size_t>(offset) > nb_file->size) || (offset + *length) > nb_file->size) {
-            return TFTP_ERR_INVALID_ARGS;
-        }
-        memcpy(nb_file->data + offset, data, *length);
-        nb_file->offset = offset + *length;
-        return TFTP_NO_ERROR;
-    } else if (file_info->type == paver) {
-        if (!atomic_load(&paving_in_progress)) {
-            printf("netsvc: paver exited prematurely with %d\n", atomic_load(&paver_exit_code));
-            atomic_store(&paver_exit_code, 0);
-            return TFTP_ERR_IO;
-        }
-
-        if ((static_cast<size_t>(offset) > file_info->paver.size) ||
-            (offset + *length) > file_info->paver.size) {
-            return TFTP_ERR_INVALID_ARGS;
-        }
-        memcpy(&file_info->paver.buffer[offset], data, *length);
-        size_t new_offset = offset + *length;
-        atomic_store(&file_info->paver.offset, new_offset);
-        // Wake the paver thread, if it is waiting for data
-        sync_completion_signal(&file_info->paver.data_ready);
-        return TFTP_NO_ERROR;
-    } else if (file_info->type == board_name) {
-        return check_board_name((const char*)data, *length)
-                   ? TFTP_NO_ERROR
-                   : TFTP_ERR_BAD_STATE;
-    } else {
-        ssize_t write_result =
-            netfile_offset_write(reinterpret_cast<const char*>(data), offset, *length);
-        if (static_cast<size_t>(write_result) == *length) {
-            return TFTP_NO_ERROR;
-        }
-        if (write_result == -EBADF) {
-            return TFTP_ERR_BAD_STATE;
-        }
-        return TFTP_ERR_IO;
-    }
-}
-
-static void file_close(void* cookie) {
-    file_info_t* file_info = reinterpret_cast<file_info_t*>(cookie);
-    if (file_info->type == netboot && file_info->netboot_file == NULL) {
-        netfile_close();
-    } else if (file_info->type == paver) {
-        unsigned int refcount = std::atomic_fetch_sub(&file_info->paver.buf_refcount, 1u);
-        if (refcount == 1) {
-            dealloc_paver_buffer(file_info);
-        }
-    }
-}
-
-static tftp_status transport_send(void* data, size_t len, void* transport_cookie) {
+tftp_status transport_send(void* data, size_t len, void* transport_cookie) {
     transport_info_t* transport_info = reinterpret_cast<transport_info_t*>(transport_cookie);
     zx_status_t status = udp6_send(data, len, &transport_info->dest_addr, transport_info->dest_port,
                                    NB_TFTP_OUTGOING_PORT, true);
@@ -461,28 +82,30 @@
     // The timeout is relative to sending instead of receiving a packet, since there are some
     // received packets we want to ignore (duplicate ACKs).
     if (transport_info->timeout_ms != 0) {
-        tftp_next_timeout = zx_deadline_after(ZX_MSEC(transport_info->timeout_ms));
+        g_tftp_next_timeout = zx_deadline_after(ZX_MSEC(transport_info->timeout_ms));
         update_timeouts();
     }
     return TFTP_NO_ERROR;
 }
 
-static int transport_timeout_set(uint32_t timeout_ms, void* transport_cookie) {
+int transport_timeout_set(uint32_t timeout_ms, void* transport_cookie) {
     transport_info_t* transport_info = reinterpret_cast<transport_info_t*>(transport_cookie);
     transport_info->timeout_ms = timeout_ms;
     return 0;
 }
 
-extern bool xfer_active;
-
-static void initialize_connection(const ip6_addr_t* saddr, uint16_t sport) {
+void initialize_connection(const ip6_addr_t* saddr, uint16_t sport) {
     int ret = tftp_init(&session, tftp_session_scratch, sizeof(tftp_session_scratch));
     if (ret != TFTP_NO_ERROR) {
         printf("netsvc: failed to initiate tftp session\n");
-        session = NULL;
+        session = nullptr;
         return;
     }
 
+    if (g_file_api == nullptr) {
+        g_file_api = new netsvc::FileApi(netbootloader());
+    }
+
     // Initialize file interface
     tftp_file_interface file_ifc = {file_open_read, file_open_write, file_read, file_write,
                                     file_close};
@@ -498,26 +121,35 @@
     xfer_active = true;
 }
 
-static void end_connection() {
-    session = NULL;
-    tftp_next_timeout = ZX_TIME_INFINITE;
+void end_connection() {
+    session = nullptr;
+    g_tftp_next_timeout = ZX_TIME_INFINITE;
     xfer_active = false;
 }
 
+void report_metrics() {
+    char buf[256];
+    if (session && tftp_get_metrics(session, buf, sizeof(buf)) == TFTP_NO_ERROR) {
+        printf("netsvc: metrics: %s\n", buf);
+    }
+}
+
+} // namespace
+
+zx_time_t tftp_next_timeout() { return g_tftp_next_timeout; }
+
 void tftp_timeout_expired() {
     tftp_status result =
         tftp_timeout(session, tftp_out_scratch, &last_msg_size, sizeof(tftp_out_scratch),
-                     &transport_info.timeout_ms, &file_info);
+                     &transport_info.timeout_ms, g_file_api);
     if (result == TFTP_ERR_TIMED_OUT) {
         printf("netsvc: excessive timeouts, dropping tftp connection\n");
-        file_close(&file_info);
+        g_file_api->Abort();
         end_connection();
-        netfile_abort_write();
     } else if (result < 0) {
         printf("netsvc: failed to generate timeout response, dropping tftp connection\n");
-        file_close(&file_info);
+        g_file_api->Abort();
         end_connection();
-        netfile_abort_write();
     } else {
         if (last_msg_size > 0) {
             tftp_status send_result =
@@ -529,13 +161,6 @@
     }
 }
 
-static void report_metrics() {
-    char buf[256];
-    if (session && tftp_get_metrics(session, buf, sizeof(buf)) == TFTP_NO_ERROR) {
-        printf("netsvc: metrics: %s\n", buf);
-    }
-}
-
 void tftp_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport,
                const ip6_addr_t* saddr, uint16_t sport) {
     if (dport == NB_TFTP_INCOMING_PORT) {
@@ -560,21 +185,20 @@
                                       .outbuf_sz = &last_msg_size,
                                       .err_msg = err_msg,
                                       .err_msg_sz = sizeof(err_msg)};
-    tftp_status status = tftp_handle_msg(session, &transport_info, &file_info, &handler_opts);
+    tftp_status status = tftp_handle_msg(session, &transport_info, g_file_api, &handler_opts);
     switch (status) {
     case TFTP_NO_ERROR:
         return;
     case TFTP_TRANSFER_COMPLETED:
-        printf("netsvc: tftp %s of file %s completed\n", file_info.is_write ? "write" : "read",
-               file_info.filename);
+        printf("netsvc: tftp %s of file %s completed\n", g_file_api->is_write() ? "write" : "read",
+               g_file_api->filename());
         report_metrics();
         break;
     case TFTP_ERR_SHOULD_WAIT:
         break;
     default:
         printf("netsvc: %s\n", err_msg);
-        netfile_abort_write();
-        file_close(&file_info);
+        g_file_api->Abort();
         report_metrics();
         break;
     }
@@ -588,7 +212,7 @@
 void tftp_send_next() {
     last_msg_size = sizeof(tftp_out_scratch);
     tftp_prepare_data(session, tftp_out_scratch, &last_msg_size, &transport_info.timeout_ms,
-                      &file_info);
+                      g_file_api);
     if (last_msg_size) {
         transport_send(tftp_out_scratch, last_msg_size, &transport_info);
     }
diff --git a/zircon/system/core/netsvc/tftp.h b/zircon/system/core/netsvc/tftp.h
new file mode 100644
index 0000000..5a5e65a
--- /dev/null
+++ b/zircon/system/core/netsvc/tftp.h
@@ -0,0 +1,19 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <inet6/inet6.h>
+
+#define TFTP_TIMEOUT_SECS 1
+
+zx_time_t tftp_next_timeout();
+
+void tftp_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport,
+               const ip6_addr_t* saddr, uint16_t sport);
+
+void tftp_timeout_expired();
+
+bool tftp_has_pending();
+void tftp_send_next();
diff --git a/zircon/system/utest/BUILD.gn b/zircon/system/utest/BUILD.gn
index f6ecff4..ef284e2 100644
--- a/zircon/system/utest/BUILD.gn
+++ b/zircon/system/utest/BUILD.gn
@@ -23,6 +23,7 @@
       "$zx/system/core/devmgr/devhost:devhost-test",
       "$zx/system/core/devmgr/fshost:block-watcher-test",
       "$zx/system/core/devmgr/fshost:fshost-test",
+      "$zx/system/core/netsvc:netsvc-test",
       "$zx/system/core/svchost:crashsvc-test",
       "$zx/system/core/virtcon:virtual-console-test",
       "$zx/system/dev/backlight/sg-micro:sgm37603a-test",