Revert "[netstack] Route table re-architecture"

This reverts commit 52059d0841223d5c3001d1204d37e511f747cb28.

Reason for revert: breaks global integration

Original change's description:
> [netstack] Route table re-architecture
> 
> Note: Part of the code implements a soft transition in the netstack.fidl to
> avoid breaking Chromium. The soft transition copies and extends the
> NetInterface and RouteTableEntry structs as well as the functions using them.
> Once Chromium transitions away from netstack.fidl this can be removed.
> 
> The main change cleans up and improves the route table logic by doing several
> things:
> 
> 1)
> The per-interface route tables stored in netiface.NIC are removed and
> consolidated in its own route_table package and RouteTable type.
> 
> 2)
> The fuchsia.netstack.fidl RouteTableTransaction interface is simplified by
> replacing the current {Get,Set}RouteTable and Commit functions with simpler
> {Add,Del}Route ones. This also matches the {Add,Del}ForwardingEntry APIs in the
> new fuchsia.net.stack nicely.
> 
> 3)
> 
> The RouteTable type implements a sorted table of routes that carry additional
> attributes besides the standard tcpip.Route, most importantly a metric value
> that is used as a tie-breaker when sorting the table. The metric can either be
> statically chosen by the user, or dynamically chosen by tracking the metric of
> the interface the route points to. An according metric value is added to the
> netiface.NIC struct, which can be overwritten via ifconfig command. This allows
> to favor defaults routes going out a specific interface. E.g., if both WLAN and
> Ethernet ports have obtained IP addresses and gateways via DHCP, setting a
> lower metric on the WLAN interface can favor it over the Ethernet one so its
> default route is sorted above the Ethernet's.
> 
> In addition, static IPs assigned to an interface as well as their subnet routes
> are not removed anymore when the interface goes down. Rather, Netstack
> remembers whether the address was obtained dynamically (DHCP) or statically and
> only removes the dynamic one. Static IPs remain assigned, but its subnet route
> disabled via an extended route attribute, so it isn't used by gVisor Netstack.
> Eventually, this behavior should be coming from netcfg.
> 
> NET-1773 #done
> NET-1916 #done
> NET-2054 #done
> NET-1978
> NET-1223
> 
> Tested:
> - Unit-tests: fx run-test netstack_gotests
> 
> - Manual tests on device:
>   Plug eth04p into MacBook, configure static IP on MacBook:
>    sudo ifconfig en8 inet 192.168.10.2 netmask 255.255.255.0
> 
>   On Toulouse:
>    fx shell ifconfig ethp04 up
>    fx shell ifconfig ethp04
>    (should be UP, metric 100)
> 
>    fx shell ifconfig ethp04 add 192.168.10.3/24 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>    (verify new IP is set, and new "192.168.10.0/24 via ethp04 metric 100" route entry in the proper place)
> 
>    ping 192.168.10.2
>     (pinging MacBook succeeds, same goes for the other direction pinging
>      192.168.10.3 from MacBook)
> 
>    fx shell ifconfig ethp04 down && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (IP address and route remain, but are disabled)
> 
>    ping 192.168.10.2
>     (Pinging either direction doesn't work)
> 
>    fx shell ifconfig ethp04 up && fx shell ifconfig ethp04 up && \
>      fx shell ifconfig ethp04 && fx shell ifconfig route show
>     (IP and route still there, re-enabled)
> 
>    ping 192.168.10.2
>     (pinging works again in both directions)
> 
>    fx shell ifconfig ethp04 metric 50 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (if and route metric changed to 50, route moved up one spot)
> 
>    fx shell ifconfig route add 192.168.20.0/24 iface ethp04 && \
>      fx shell ifconfig ethp04 && fx shell ifconfig route show
>     (Route is added to the table, no gateway, metric is 50 from ethp04)
> 
>    fx shell ifconfig route add 192.168.30.0/24 iface ethp04 gateway \
>      192.168.30.1 metric 150 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (Route is added, with gateway, metric is 150, not tracking ethp04, sorted
>     below the other routes on ethp04 due to higher metric)
> 
>    fx shell ifconfig route add 0.0.0.0/0 iface ethp04 gateway 192.168.30.1 \
>      metric 110 && fx shell ifconfig ethp04 && fx shell ifconfig route show
>     (Default is added, with gateway 192.168.30.1, metric 110 not tracking
>     ethp04, sorted below other default routes due to higher metric)
> 
>    fx shell ifconfig ethp04 metric 200 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (all routes tracking ethp04's are updated to metric=200 and the table is
>     resorted)
> 
>     fx shell ifconfig route del 0.0.0.0/0 iface ethp04 && \
>       fx shell ifconfig ethp04 && fx shell ifconfig route show
>     (Removes default route on ethp04)
> 
>    fx shell ifconfig route del 192.168.30.0/24 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
> 
>    fx shell ifconfig route del 192.168.20.0/24 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (Removes 192.168.{30,20}/24 routes)
> 
>    fx shell ifconfig ethp04 del 192.168.10.3/24 && fx shell ifconfig ethp04 && \
>      fx shell ifconfig route show
>     (IP and route are removed)
> 
> Change-Id: Id6b4232986493ec313d29d4f236461c491eeda2e

TBR=stijlist@google.com,tamird@google.com,eyalsoha@google.com,brunodalbo@google.com,ckuiper@google.com

Change-Id: I70843342680d315447b29db62de4b83f6bb7754c
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
diff --git a/garnet/bin/guest/integration/mock_netstack.h b/garnet/bin/guest/integration/mock_netstack.h
index 3d7a448..dd9ac82 100644
--- a/garnet/bin/guest/integration/mock_netstack.h
+++ b/garnet/bin/guest/integration/mock_netstack.h
@@ -21,10 +21,8 @@
                   GetAddressCallback callback) override {}
 
   void GetInterfaces(GetInterfacesCallback callback) override {}
-  void GetInterfaces2(GetInterfaces2Callback callback) override {}
 
   void GetRouteTable(GetRouteTableCallback callback) override {}
-  void GetRouteTable2(GetRouteTable2Callback callback) override {}
 
   void GetStats(uint32_t nicid, GetStatsCallback callback) override {}
 
@@ -40,9 +38,6 @@
       uint32_t nicid, fuchsia::net::IpAddress addr, uint8_t prefixLen,
       RemoveInterfaceAddressCallback callback) override {}
 
-  void SetInterfaceMetric(uint32_t nicid, uint32_t metric,
-                          SetInterfaceMetricCallback callback) override {}
-
   void SetDhcpClientStatus(uint32_t nicid, bool enabled,
                            SetDhcpClientStatusCallback callback) override {}
 
@@ -79,4 +74,4 @@
   uintptr_t io_addr_;
 };
 
-#endif  // GARNET_BIN_GUEST_INTEGRATION_MOCK_NETSTACK_H_
+#endif  // GARNET_BIN_GUEST_INTEGRATION_MOCK_NETSTACK_H_
\ No newline at end of file
diff --git a/garnet/bin/guest/vmm/device/virtio_net_test.cc b/garnet/bin/guest/vmm/device/virtio_net_test.cc
index 8f628d5..792302be 100644
--- a/garnet/bin/guest/vmm/device/virtio_net_test.cc
+++ b/garnet/bin/guest/vmm/device/virtio_net_test.cc
@@ -30,10 +30,8 @@
                   GetAddressCallback callback) override {}
 
   void GetInterfaces(GetInterfacesCallback callback) override {}
-  void GetInterfaces2(GetInterfaces2Callback callback) override {}
 
   void GetRouteTable(GetRouteTableCallback callback) override {}
-  void GetRouteTable2(GetRouteTable2Callback callback) override {}
 
   void GetStats(uint32_t nicid, GetStatsCallback callback) override {}
 
@@ -49,9 +47,6 @@
       uint32_t nicid, fuchsia::net::IpAddress addr, uint8_t prefixLen,
       RemoveInterfaceAddressCallback callback) override {}
 
-  void SetInterfaceMetric(uint32_t nicid, uint32_t metric,
-                          SetInterfaceMetricCallback callback) override {}
-
   void SetDhcpClientStatus(uint32_t nicid, bool enabled,
                            SetDhcpClientStatusCallback callback) override {}
 
diff --git a/garnet/bin/netcfg/src/main.rs b/garnet/bin/netcfg/src/main.rs
index ea86ad1..70ddf2b 100644
--- a/garnet/bin/netcfg/src/main.rs
+++ b/garnet/bin/netcfg/src/main.rs
@@ -54,7 +54,7 @@
     }
 }
 
-fn derive_device_name(interfaces: Vec<fidl_fuchsia_netstack::NetInterface2>) -> Option<String> {
+fn derive_device_name(interfaces: Vec<fidl_fuchsia_netstack::NetInterface>) -> Option<String> {
     interfaces
         .iter()
         .filter(|iface| {
@@ -102,12 +102,9 @@
 
     let mut device_name_stream = futures::stream::iter(default_device_name).chain(
         netstack.take_event_stream().try_filter_map(
-            |event| match event {
-                fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged2 { interfaces } => {
-                    futures::future::ok(derive_device_name(interfaces).map(Cow::Owned))
-                },
-                _ => futures::future::ok(None),
-            }
+            |fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged { interfaces }| {
+                futures::future::ok(derive_device_name(interfaces).map(Cow::Owned))
+            },
         ),
     );
 
diff --git a/garnet/bin/netemul_runner/helpers/netstack_cfg/src/main.rs b/garnet/bin/netemul_runner/helpers/netstack_cfg/src/main.rs
index e926639..751b15e 100644
--- a/garnet/bin/netemul_runner/helpers/netstack_cfg/src/main.rs
+++ b/garnet/bin/netemul_runner/helpers/netstack_cfg/src/main.rs
@@ -64,19 +64,13 @@
     };
 
     let mut if_changed = netstack.take_event_stream().try_filter_map(
-        |event| match event {
-            fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged2 { interfaces } => {
-                let iface = interfaces
-                    .iter()
-                    .filter(|iface| iface.name == if_name)
-                    .next();
-                match iface {
-                    None => futures::future::ok(None),
-                    Some(a) => futures::future::ok(Some((a.id, a.hwaddr.clone()))),
-                }
-            },
-            _ => futures::future::ok(None),
-        }
+        |fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged { interfaces }| {
+            let iface = interfaces.iter().filter(|iface| iface.name == if_name).next();
+            match iface {
+                None => futures::future::ok(None),
+                Some(a) => futures::future::ok(Some((a.id, a.hwaddr.clone()))),
+            }
+        },
     );
     let _nicid =
         await!(netstack.add_ethernet_device(&format!("/vdev/{}", opt.endpoint), &mut cfg, eth))
diff --git a/garnet/bin/netemul_runner/test/netstack_socks/src/child.rs b/garnet/bin/netemul_runner/test/netstack_socks/src/child.rs
index 81f21fe..e394afa 100644
--- a/garnet/bin/netemul_runner/test/netstack_socks/src/child.rs
+++ b/garnet/bin/netemul_runner/test/netstack_socks/src/child.rs
@@ -100,19 +100,13 @@
     };
 
     let mut if_changed = netstack.take_event_stream().try_filter_map(
-        |event| match event {
-            fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged2 { interfaces } => {
-                let iface = interfaces
-                    .iter()
-                    .filter(|iface| iface.name == if_name)
-                    .next();
-                match iface {
-                    None => futures::future::ok(None),
-                    Some(a) => futures::future::ok(Some((a.id, a.hwaddr.clone()))),
-                }
-            },
-            _ => futures::future::ok(None),
-        }
+        |fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged { interfaces }| {
+            let iface = interfaces.iter().filter(|iface| iface.name == if_name).next();
+            match iface {
+                None => futures::future::ok(None),
+                Some(a) => futures::future::ok(Some((a.id, a.hwaddr.clone()))),
+            }
+        },
     );
     let _nicid =
         await!(netstack.add_ethernet_device(&format!("/vdev/{}", opt.endpoint), &mut cfg, eth))
diff --git a/garnet/go/src/netstack/BUILD.gn b/garnet/go/src/netstack/BUILD.gn
index 2b31482..abe34c5 100644
--- a/garnet/go/src/netstack/BUILD.gn
+++ b/garnet/go/src/netstack/BUILD.gn
@@ -61,6 +61,7 @@
     ":netstack_fidlconv_test",
     ":netstack_filter_test",
     ":netstack_link_eth_test",
+    ":netstack_netiface_test",
     ":netstack_test",
     "netstack_service_test",
     "//garnet/go/src/netstack/util:netstack_util_test",
@@ -86,6 +87,9 @@
       name = "netstack_link_eth_test"
     },
     {
+      name = "netstack_netiface_test"
+    },
+    {
       name = "netstack_test"
     },
     {
@@ -161,6 +165,13 @@
   ]
 }
 
+go_test("netstack_netiface_test") {
+  gopackage = "netstack/netiface"
+  deps = [
+    ":netstack_lib",
+  ]
+}
+
 go_test("netstack_test") {
   gopackage = "netstack"
   deps = [
diff --git a/garnet/go/src/netstack/connectivity/connectivity.go b/garnet/go/src/netstack/connectivity/connectivity.go
index 6180137..cc00e60 100644
--- a/garnet/go/src/netstack/connectivity/connectivity.go
+++ b/garnet/go/src/netstack/connectivity/connectivity.go
@@ -11,7 +11,7 @@
 
 	"app/context"
 	"netstack/fidlconv"
-	"netstack/util"
+	"netstack/netiface"
 
 	"fidl/fuchsia/net"
 	"fidl/fuchsia/netstack"
@@ -37,7 +37,7 @@
 
 // TODO(NET-1001): extract into a separate reachability service based on a
 // better network reachability signal.
-func InferAndNotify(ifs []netstack.NetInterface2) {
+func InferAndNotify(ifs []netstack.NetInterface) {
 	if debug {
 		log.Printf("inferring network reachability")
 	}
@@ -53,11 +53,11 @@
 	mu.Unlock()
 }
 
-func hasDHCPAddress(nic netstack.NetInterface2) bool {
-	return nic.Flags&netstack.NetInterfaceFlagDhcp != 0 && nic.Flags&netstack.NetInterfaceFlagUp != 0 && !util.IsAny(fidlconv.ToTCPIPAddress(nic.Addr))
+func hasDHCPAddress(nic netstack.NetInterface) bool {
+	return nic.Flags&netstack.NetInterfaceFlagDhcp != 0 && nic.Flags&netstack.NetInterfaceFlagUp != 0 && !netiface.IsAny(fidlconv.ToTCPIPAddress(nic.Addr))
 }
 
-func inferReachability(ifs []netstack.NetInterface2) bool {
+func inferReachability(ifs []netstack.NetInterface) bool {
 	for _, nic := range ifs {
 		if hasDHCPAddress(nic) {
 			return true
diff --git a/garnet/go/src/netstack/connectivity/connectivity_test.go b/garnet/go/src/netstack/connectivity/connectivity_test.go
index 368c246..3959ee5 100644
--- a/garnet/go/src/netstack/connectivity/connectivity_test.go
+++ b/garnet/go/src/netstack/connectivity/connectivity_test.go
@@ -23,12 +23,12 @@
 func TestHasDHCPAddress(t *testing.T) {
 	for _, tc := range []struct {
 		name  string
-		iface netstack.NetInterface2
+		iface netstack.NetInterface
 		want  bool
 	}{
 		{
 			name: "DHCPEnabledNoAddress",
-			iface: netstack.NetInterface2{
+			iface: netstack.NetInterface{
 				Flags:     netstack.NetInterfaceFlagDhcp | netstack.NetInterfaceFlagUp,
 				Addr:      newV4Address(0, 0, 0, 0),
 				Netmask:   newV4Address(0, 0, 0, 0),
@@ -39,7 +39,7 @@
 		},
 		{
 			name: "StaticAddress",
-			iface: netstack.NetInterface2{
+			iface: netstack.NetInterface{
 				Flags:     netstack.NetInterfaceFlagUp,
 				Addr:      newV4Address(192, 168, 42, 10),
 				Netmask:   newV4Address(255, 255, 255, 0),
@@ -50,7 +50,7 @@
 		},
 		{
 			name: "DHCPEnabledWithAddress",
-			iface: netstack.NetInterface2{
+			iface: netstack.NetInterface{
 				Flags:     netstack.NetInterfaceFlagDhcp | netstack.NetInterfaceFlagUp,
 				Addr:      newV4Address(10, 0, 0, 1),
 				Netmask:   newV4Address(255, 255, 255, 0),
diff --git a/garnet/go/src/netstack/ifconfig/ifconfig.go b/garnet/go/src/netstack/ifconfig/ifconfig.go
index c0226450..6287aa2 100644
--- a/garnet/go/src/netstack/ifconfig/ifconfig.go
+++ b/garnet/go/src/netstack/ifconfig/ifconfig.go
@@ -8,7 +8,6 @@
 	"fmt"
 	"net"
 	"os"
-	"strconv"
 	"strings"
 	"syscall/zx"
 
@@ -30,7 +29,7 @@
 }
 
 func (a *netstackClientApp) printAll() {
-	ifaces, err := a.netstack.GetInterfaces2()
+	ifaces, err := a.netstack.GetInterfaces()
 	if err != nil {
 		fmt.Print("ifconfig: failed to fetch interfaces\n")
 		return
@@ -41,8 +40,8 @@
 	}
 }
 
-func getIfaceByNameFromIfaces(name string, ifaces []netstack.NetInterface2) *netstack.NetInterface2 {
-	var candidate *netstack.NetInterface2
+func getIfaceByNameFromIfaces(name string, ifaces []netstack.NetInterface) *netstack.NetInterface {
+	var candidate *netstack.NetInterface
 	for i, iface := range ifaces {
 		if strings.HasPrefix(iface.Name, name) {
 			if candidate != nil {
@@ -54,7 +53,7 @@
 	return candidate
 }
 
-func getIfaceByIdFromIfaces(id uint32, ifaces []netstack.NetInterface2) *netstack.NetInterface2 {
+func getIfaceByIdFromIfaces(id uint32, ifaces []netstack.NetInterface) *netstack.NetInterface {
 	for _, iface := range ifaces {
 		if iface.Id == id {
 			return &iface
@@ -63,7 +62,7 @@
 	return nil
 }
 
-func (a *netstackClientApp) printIface(iface netstack.NetInterface2) {
+func (a *netstackClientApp) printIface(iface netstack.NetInterface) {
 	stats, err := a.netstack.GetStats(iface.Id)
 
 	if err != nil {
@@ -77,7 +76,6 @@
 		// TODO: scopes
 		fmt.Printf("\tinet6 addr: %s/%d Scope:Link\n", netAddrToString(addr.Addr), addr.PrefixLen)
 	}
-	fmt.Printf("\tmetric:%d\n", iface.Metric)
 	fmt.Printf("\t%s\n", flagsToString(iface.Flags))
 
 	if isWLAN(iface.Features) {
@@ -92,11 +90,11 @@
 	// TODO: more stats. MTU, RX/TX errors
 }
 
-func (a *netstackClientApp) setStatus(iface netstack.NetInterface2, up bool) {
+func (a *netstackClientApp) setStatus(iface netstack.NetInterface, up bool) {
 	a.netstack.SetInterfaceStatus(iface.Id, up)
 }
 
-func (a *netstackClientApp) addIfaceAddress(iface netstack.NetInterface2, cidr string) {
+func (a *netstackClientApp) addIfaceAddress(iface netstack.NetInterface, cidr string) {
 	netAddr, prefixLen := validateCidr(os.Args[3])
 	result, _ := a.netstack.SetInterfaceAddress(iface.Id, netAddr, prefixLen)
 	if result.Status != netstack.StatusOk {
@@ -104,7 +102,7 @@
 	}
 }
 
-func (a *netstackClientApp) removeIfaceAddress(iface netstack.NetInterface2, cidr string) {
+func (a *netstackClientApp) removeIfaceAddress(iface netstack.NetInterface, cidr string) {
 	netAddr, prefixLen := validateCidr(os.Args[3])
 	result, _ := a.netstack.RemoveInterfaceAddress(iface.Id, netAddr, prefixLen)
 	if result.Status != netstack.StatusOk {
@@ -112,23 +110,16 @@
 	}
 }
 
-func (a *netstackClientApp) parseRouteAttribute(in *netstack.RouteTableEntry2, args []string) (remaining []string, err error) {
+func (a *netstackClientApp) parseRouteAttribute(in *netstack.RouteTableEntry, args []string) (remaining []string, err error) {
 	if len(args) < 2 {
 		return args, fmt.Errorf("not enough args to make attribute")
 	}
 	var attr, val string
 	switch attr, val, remaining = args[0], args[1], args[2:]; attr {
 	case "gateway":
-		gateway := toIpAddress(net.ParseIP(val))
-		in.Gateway = &gateway
-	case "metric":
-		m, err := strconv.ParseUint(val, 10, 32)
-		if err != nil {
-			return remaining, fmt.Errorf("metric value '%s' is not uint32: %s", val, err)
-		}
-		in.Metric = uint32(m)
+		in.Gateway = toIpAddress(net.ParseIP(val))
 	case "iface":
-		ifaces, err := a.netstack.GetInterfaces2()
+		ifaces, err := a.netstack.GetInterfaces()
 		if err != nil {
 			return remaining, err
 		}
@@ -148,7 +139,7 @@
 	return remaining, nil
 }
 
-func (a *netstackClientApp) newRouteFromArgs(args []string) (route netstack.RouteTableEntry2, err error) {
+func (a *netstackClientApp) newRouteFromArgs(args []string) (route netstack.RouteTableEntry, err error) {
 	destination, remaining := args[0], args[1:]
 	dstAddr, dstSubnet, err := net.ParseCIDR(destination)
 	if err != nil {
@@ -172,11 +163,10 @@
 	return route, nil
 }
 
-func (a *netstackClientApp) addRoute(r netstack.RouteTableEntry2) error {
-	if r.Gateway == nil && r.Nicid == 0 {
+func (a *netstackClientApp) addRoute(r netstack.RouteTableEntry) error {
+	if (r.Gateway == netfidl.IpAddress{}) && r.Nicid == 0 {
 		return fmt.Errorf("either gateway or iface must be provided when adding a route")
 	}
-
 	req, transactionInterface, err := netstack.NewRouteTableTransactionInterfaceRequest()
 	if err != nil {
 		return fmt.Errorf("could not make a new route table transaction: %s", err)
@@ -186,18 +176,58 @@
 	if err != nil || zx.Status(status) != zx.ErrOk {
 		return fmt.Errorf("could not start a route table transaction: %s (%s)", err, zx.Status(status))
 	}
-
-	status, err = transactionInterface.AddRoute(r)
+	rs, err := transactionInterface.GetRouteTable()
 	if err != nil {
-		return fmt.Errorf("could not add route due to transaction interface error: %s", err)
+		return fmt.Errorf("could not get route table from netstack: %s", err)
 	}
-	if zx.Status(status) != zx.ErrOk {
-		return fmt.Errorf("could not add route in netstack: %s", zx.Status(status))
+	err = transactionInterface.SetRouteTable(append(rs, r))
+	if err != nil {
+		return fmt.Errorf("could not set route table in netstack: %s", err)
+	}
+	status, err = transactionInterface.Commit()
+	if err != nil || zx.Status(status) != zx.ErrOk {
+		return fmt.Errorf("could not commit route table in netstack: %s (%s)", err, zx.Status(status))
 	}
 	return nil
 }
 
-func (a *netstackClientApp) deleteRoute(r netstack.RouteTableEntry2) error {
+func equalIpAddress(a netfidl.IpAddress, b netfidl.IpAddress) bool {
+	if a.Which() != b.Which() {
+		return false
+	}
+	switch a.Which() {
+	case netfidl.IpAddressIpv4:
+		return a.Ipv4.Addr == b.Ipv4.Addr
+	case netfidl.IpAddressIpv6:
+		return a.Ipv6.Addr == b.Ipv6.Addr
+	default:
+		return false
+	}
+}
+
+// Returns true if the target matches the source.  If the target is
+// missing the gateway or nicid then those are considered to be
+// matching.
+func matchRoute(target netstack.RouteTableEntry, source netstack.RouteTableEntry) bool {
+	if !equalIpAddress(target.Destination, source.Destination) {
+		return false
+	}
+	if !equalIpAddress(target.Netmask, source.Netmask) {
+		return false
+	}
+	if target.Gateway.Which() != 0 &&
+		!equalIpAddress(source.Gateway, target.Gateway) {
+		// The gateway is neither wildcard nor a match.
+		return false
+	}
+	if target.Nicid != 0 && source.Nicid != target.Nicid {
+		// The Nicid is neither wildcard nor a match.
+		return false
+	}
+	return true
+}
+
+func (a *netstackClientApp) deleteRoute(target netstack.RouteTableEntry) error {
 	req, transactionInterface, err := netstack.NewRouteTableTransactionInterfaceRequest()
 	if err != nil {
 		return fmt.Errorf("could not make a new route table transaction: %s", err)
@@ -208,17 +238,24 @@
 		return fmt.Errorf("could not start a route table transaction (maybe the route table is locked?): %s", err)
 	}
 
-	status, err = transactionInterface.DelRoute(r)
+	rs, err := transactionInterface.GetRouteTable()
 	if err != nil {
-		return fmt.Errorf("could not delete route due to transaction interface error: %s", err)
+		return fmt.Errorf("could not get route table from netstack: %s", err)
 	}
-	if zx.Status(status) != zx.ErrOk {
-		return fmt.Errorf("could not delete route in netstack: %s", zx.Status(status))
+	for i, r := range rs {
+		if matchRoute(target, r) {
+			transactionInterface.SetRouteTable(append(rs[:i], rs[i+1:]...))
+			_, err = transactionInterface.Commit()
+			if err != nil {
+				return fmt.Errorf("could not commit route table in netstack: %s", err)
+			}
+			return nil
+		}
 	}
-	return nil
+	return fmt.Errorf("could not find route to delete in route table")
 }
 
-func routeTableEntryToString(r netstack.RouteTableEntry2, ifaces []netstack.NetInterface2) string {
+func routeTableEntryToString(r netstack.RouteTableEntry, ifaces []netstack.NetInterface) string {
 	iface := getIfaceByIdFromIfaces(r.Nicid, ifaces)
 	var ifaceName string
 	if iface == nil {
@@ -233,18 +270,15 @@
 	case netfidl.IpAddressIpv6:
 		netAndMask = net.IPNet{IP: r.Destination.Ipv6.Addr[:], Mask: r.Netmask.Ipv6.Addr[:]}
 	}
-	if r.Gateway != nil {
-		return fmt.Sprintf("%s via %s %s metric %v", netAndMask.String(), netAddrToString(*r.Gateway), ifaceName, r.Metric)
-	}
-	return fmt.Sprintf("%s via %s metric %v", netAndMask.String(), ifaceName, r.Metric)
+	return fmt.Sprintf("%s via %s %s", netAndMask.String(), netAddrToString(r.Gateway), ifaceName)
 }
 
 func (a *netstackClientApp) showRoutes() error {
-	rs, err := a.netstack.GetRouteTable2()
+	rs, err := a.netstack.GetRouteTable()
 	if err != nil {
 		return fmt.Errorf("Could not get route table from netstack: %s", err)
 	}
-	ifaces, err := a.netstack.GetInterfaces2()
+	ifaces, err := a.netstack.GetInterfaces()
 	if err != nil {
 		return err
 	}
@@ -255,10 +289,10 @@
 }
 
 func (a *netstackClientApp) bridge(ifNames []string) error {
-	ifs := make([]*netstack.NetInterface2, len(ifNames))
+	ifs := make([]*netstack.NetInterface, len(ifNames))
 	nicIDs := make([]uint32, len(ifNames))
 	// first, validate that all interfaces exist
-	ifaces, err := a.netstack.GetInterfaces2()
+	ifaces, err := a.netstack.GetInterfaces()
 	if err != nil {
 		return err
 	}
@@ -279,7 +313,7 @@
 	return nil
 }
 
-func (a *netstackClientApp) setDHCP(iface netstack.NetInterface2, startStop string) {
+func (a *netstackClientApp) setDHCP(iface netstack.NetInterface, startStop string) {
 	switch startStop {
 	case "start":
 		a.netstack.SetDhcpClientStatus(iface.Id, true)
@@ -411,11 +445,10 @@
 func usage() {
 	fmt.Printf("Usage:\n")
 	fmt.Printf("  %s [<interface>]\n", os.Args[0])
-	fmt.Printf("  %s <interface> {up|down}\n", os.Args[0])
-	fmt.Printf("  %s <interface> {add|del} <address>/<mask>\n", os.Args[0])
-	fmt.Printf("  %s <interface> metric <metric>\n", os.Args[0])
-	fmt.Printf("  %s <interface> dhcp {start|stop}\n", os.Args[0])
-	fmt.Printf("  %s route {add|del} <address>/<mask> [iface <name>] [gateway <address>] [metric <metric>]\n", os.Args[0])
+	fmt.Printf("  %s [<interface>] [up|down]\n", os.Args[0])
+	fmt.Printf("  %s [<interface>] [add|del] [<address>]/[<mask>]\n", os.Args[0])
+	fmt.Printf("  %s [<interface>] dhcp [start|stop]\n", os.Args[0])
+	fmt.Printf("  %s route [add|del] [<address>/<mask>] [iface <name>] [gateway <address>/<mask>]\n", os.Args[0])
 	fmt.Printf("  %s route show\n", os.Args[0])
 	fmt.Printf("  %s bridge [<interface>]+\n", os.Args[0])
 	os.Exit(1)
@@ -443,7 +476,7 @@
 		return
 	}
 
-	var iface *netstack.NetInterface2
+	var iface *netstack.NetInterface
 	switch os.Args[1] {
 	case "route":
 		if len(os.Args) == 2 {
@@ -472,17 +505,17 @@
 		switch op {
 		case "add":
 			if err = a.addRoute(r); err != nil {
-				fmt.Printf("Error adding route to route table: %s\n", err)
+				fmt.Printf("Error adding route to route table: %s", err)
 				usage()
 			}
 		case "del":
 			err = a.deleteRoute(r)
 			if err != nil {
-				fmt.Printf("Error deleting route from route table: %s\n", err)
+				fmt.Printf("Error deleting route from route table: %s", err)
 				usage()
 			}
 		default:
-			fmt.Printf("Unknown route operation: %s\n", op)
+			fmt.Printf("Unknown route operation: %s", op)
 			usage()
 		}
 
@@ -491,7 +524,7 @@
 		ifaces := os.Args[2:]
 		err := a.bridge(ifaces)
 		if err != nil {
-			fmt.Printf("error creating bridge: %s\n", err)
+			fmt.Printf("error creating bridge: %s", err)
 		} else {
 			fmt.Printf("Bridged interfaces %s\n", ifaces)
 		}
@@ -500,7 +533,7 @@
 		usage()
 		return
 	default:
-		ifaces, err := a.netstack.GetInterfaces2()
+		ifaces, err := a.netstack.GetInterfaces()
 		if err != nil {
 			fmt.Printf("Error finding interface name: %s\n", err)
 			return
@@ -519,30 +552,18 @@
 	switch len(os.Args) {
 	case 2:
 		a.printIface(*iface)
-	case 3:
+	case 3, 4:
 		switch os.Args[2] {
 		case "up":
 			a.setStatus(*iface, true)
 		case "down":
 			a.setStatus(*iface, false)
-		default:
-			usage()
-		}
-	case 4:
-		switch os.Args[2] {
 		case "add":
 			a.addIfaceAddress(*iface, os.Args[3])
 		case "del":
 			a.removeIfaceAddress(*iface, os.Args[3])
 		case "dhcp":
 			a.setDHCP(*iface, os.Args[3])
-		case "metric":
-			metric, err := strconv.ParseUint(os.Args[3], 10, 32)
-			if err != nil {
-				fmt.Printf("ifconfig: metric value '%s' is not uint32: %s\n", os.Args[3], err)
-				return
-			}
-			a.netstack.SetInterfaceMetric(iface.Id, uint32(metric))
 		default:
 			usage()
 		}
diff --git a/garnet/go/src/netstack/ifconfig/ifconfig_test.go b/garnet/go/src/netstack/ifconfig/ifconfig_test.go
index 8f5e4bd..f1eaeb86 100644
--- a/garnet/go/src/netstack/ifconfig/ifconfig_test.go
+++ b/garnet/go/src/netstack/ifconfig/ifconfig_test.go
@@ -97,7 +97,7 @@
 		if err == nil {
 			t.Errorf("ifconfig route add returned success with neither gateway or iface specified. output: \n%s", out)
 		}
-		expected = "Error adding route to route table"
+		expected = "Error adding route"
 		if !strings.Contains(string(out), expected) {
 			t.Errorf("want `ifconfig route add` to print \"%s\", got \"%s\"", expected, out)
 		}
diff --git a/garnet/go/src/netstack/main.go b/garnet/go/src/netstack/main.go
index a56fd2c..a35761a 100644
--- a/garnet/go/src/netstack/main.go
+++ b/garnet/go/src/netstack/main.go
@@ -80,10 +80,8 @@
 
 	var netstackService netstack.NetstackService
 
-	ns.OnInterfacesChanged = func(interfaces2 []netstack.NetInterface2) {
-		connectivity.InferAndNotify(interfaces2)
-		// Handle deprecated version OnInterfacesChanged first.
-		interfaces := interfaces2ListToInterfacesList(interfaces2)
+	ns.OnInterfacesChanged = func(interfaces []netstack.NetInterface) {
+		connectivity.InferAndNotify(interfaces)
 		for _, key := range netstackService.BindingKeys() {
 			if p, ok := netstackService.EventProxyFor(key); ok {
 				if err := p.OnInterfacesChanged(interfaces); err != nil {
@@ -91,15 +89,6 @@
 				}
 			}
 		}
-		// Now the new OnInterfacesChanged2 version.
-		// TODO(NET-2078): Remove this once Chromium stops using netstack.fidl.
-		for _, key := range netstackService.BindingKeys() {
-			if p, ok := netstackService.EventProxyFor(key); ok {
-				if err := p.OnInterfacesChanged2(interfaces2); err != nil {
-					log.Printf("OnInterfacesChanged2 failed: %v", err)
-				}
-			}
-		}
 	}
 
 	var inspectService inspect.InspectService
@@ -123,16 +112,12 @@
 		// Prevents clients from having to race GetInterfaces / InterfacesChanged.
 		if p, ok := netstackService.EventProxyFor(k); ok {
 			ns.mu.Lock()
-			interfaces2 := ns.getNetInterfaces2Locked()
-			interfaces := interfaces2ListToInterfacesList(interfaces2)
+			interfaces := ns.getInterfacesLocked()
 			ns.mu.Unlock()
 
 			if err := p.OnInterfacesChanged(interfaces); err != nil {
 				log.Printf("OnInterfacesChanged failed: %v", err)
 			}
-			if err := p.OnInterfacesChanged2(interfaces2); err != nil {
-				log.Printf("OnInterfacesChanged2 failed: %v", err)
-			}
 		}
 		return nil
 	})
diff --git a/garnet/go/src/netstack/netiface/netiface.go b/garnet/go/src/netstack/netiface/netiface.go
index 803fd1e2..faea8db 100644
--- a/garnet/go/src/netstack/netiface/netiface.go
+++ b/garnet/go/src/netstack/netiface/netiface.go
@@ -5,20 +5,75 @@
 package netiface
 
 import (
-	"netstack/routes"
-
 	"github.com/google/netstack/tcpip"
 )
 
-// TODO(NET-1223):This should be moved into netstack.go.
 type NIC struct {
-	ID            tcpip.NICID
-	Addr          tcpip.Address
-	IsDynamicAddr bool
-	Name          string
-	Features      uint32
-	Metric        routes.Metric // used as a default for routes that use this NIC
-	Netmask       tcpip.AddressMask
-	DNSServers    []tcpip.Address
-	Ipv6addrs     []tcpip.Address
+	ID         tcpip.NICID
+	Addr       tcpip.Address
+	Name       string
+	Features   uint32
+	Netmask    tcpip.AddressMask
+	Routes     []tcpip.Route
+	DNSServers []tcpip.Address
+	Ipv6addrs  []tcpip.Address
+}
+
+func IsAny(a tcpip.Address) bool {
+	for i := 0; i < len(a); i++ {
+		if a[i] != 0 {
+			return false
+		}
+	}
+	return true
+}
+
+func hasGateway(r *tcpip.Route) bool {
+	return r.Gateway != ""
+}
+
+func prefixLength(mask tcpip.AddressMask) (len int) {
+	for _, b := range []byte(mask) {
+		for i := uint(0); i < 8; i++ {
+			if b&(1<<i) != 0 {
+				len++
+			} else {
+				return len
+			}
+		}
+	}
+	return len
+}
+
+// Less is the sorting function used for routes.
+func Less(ri, rj *tcpip.Route, nics map[tcpip.NICID]*NIC) bool {
+	ni, nj := nics[ri.NIC], nics[rj.NIC]
+	// If only one of them has a route for specific address (i.e. not a route
+	// for Any destination), that element should sort before the other.
+	if IsAny(ri.Destination) != IsAny(rj.Destination) {
+		return !IsAny(ri.Destination)
+	}
+	// If only one of them has a gateway, that element should sort before
+	// the other.
+	if hasGateway(ri) != hasGateway(rj) {
+		return hasGateway(ri)
+	}
+	// If both have gateways and only one is for a specific address, that
+	// element should sort before the other.
+	if IsAny(ri.Gateway) != IsAny(rj.Gateway) {
+		return !IsAny(ri.Gateway)
+	}
+	// If only one them has a NIC with an IP address, that element should sort
+	// before the other.
+	if (ni.Addr != "") != (nj.Addr != "") {
+		return ni.Addr != ""
+	}
+	// If one has a more specific netmask (longer prefix len), that element
+	// should sort before the other.
+	li, lj := prefixLength(ri.Mask), prefixLength(rj.Mask)
+	if len(ri.Mask) == len(rj.Mask) && li != lj {
+		return li > lj
+	}
+	// Otherwise, sort them by the IDs of their NICs.
+	return ri.NIC < rj.NIC
 }
diff --git a/garnet/go/src/netstack/netiface/netiface_test.go b/garnet/go/src/netstack/netiface/netiface_test.go
new file mode 100644
index 0000000..48921e0
--- /dev/null
+++ b/garnet/go/src/netstack/netiface/netiface_test.go
@@ -0,0 +1,420 @@
+// Copyright 2017 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package netiface_test
+
+import (
+	"net"
+	"sort"
+	"strings"
+	"testing"
+
+	"netstack/fidlconv"
+	"netstack/netiface"
+	"netstack/util"
+
+	netfidl "fidl/fuchsia/net"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/netstack/tcpip"
+)
+
+func indexedByID(nics []netiface.NIC) map[tcpip.NICID]*netiface.NIC {
+	res := make(map[tcpip.NICID]*netiface.NIC)
+	for i, v := range nics {
+		res[v.ID] = &nics[i]
+	}
+	return res
+}
+
+func setID(n *netiface.NIC, id tcpip.NICID) {
+	n.ID = id
+	for i := range n.Routes {
+		n.Routes[i].NIC = id
+	}
+}
+
+func setGateway(n *netiface.NIC, gateway tcpip.Address) {
+	for i := range n.Routes {
+		n.Routes[i].Gateway = gateway
+	}
+}
+
+func NewLoopback() netiface.NIC {
+	return netiface.NIC{
+		Addr: util.Parse("127.0.0.1"),
+		Routes: []tcpip.Route{
+			{
+				Destination: util.Parse("127.0.0.1"),
+				Mask:        "\xff\xff\xff\xff",
+			},
+			{
+				Destination: util.Parse("::1"),
+				Mask:        tcpip.AddressMask(strings.Repeat("\xff", 16)),
+			},
+		},
+	}
+}
+
+func NewAnyDest() netiface.NIC {
+	return netiface.NIC{
+		Routes: []tcpip.Route{
+			{
+				Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+				Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			},
+			{
+				Destination: util.Parse("::"),
+				Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			},
+		},
+	}
+}
+
+func TestLoopbackBeforeAny(t *testing.T) {
+	var nics []netiface.NIC
+
+	// Create NIC 1 for any destination.
+	n := NewAnyDest()
+	setID(&n, 1)
+	nics = append(nics, n)
+
+	// Create NIC 2 for loopback.
+	n = NewLoopback()
+	setID(&n, 2)
+	nics = append(nics, n)
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	expected := []tcpip.Route{
+		{
+			Destination: util.Parse("127.0.0.1"),
+			Mask:        "\xff\xff\xff\xff",
+			NIC:         2,
+		},
+		{
+			Destination: util.Parse("::1"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\xff", 16)),
+			NIC:         2,
+		},
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         1,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         1,
+		},
+	}
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func TestGatewayBeforeAddr(t *testing.T) {
+	var nics []netiface.NIC
+
+	// Create NIC 1 for any destination with an address.
+	n := NewAnyDest()
+	setID(&n, 1)
+	n.Addr = util.Parse("1.2.3.4")
+	nics = append(nics, n)
+
+	// Create NIC 2 for any destination with a gateway.
+	n = NewAnyDest()
+	setID(&n, 2)
+	setGateway(&n, util.Parse("1.1.1.1"))
+	nics = append(nics, n)
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	expected := []tcpip.Route{
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			Gateway:     util.Parse("1.1.1.1"),
+			NIC:         2,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			Gateway:     util.Parse("1.1.1.1"),
+			NIC:         2,
+		},
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         1,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         1,
+		},
+	}
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func TestAddrBeforeAny(t *testing.T) {
+	var nics []netiface.NIC
+
+	// Create NIC 1 for any destination.
+	n := NewAnyDest()
+	setID(&n, 1)
+	nics = append(nics, n)
+
+	// Create NIC 2 for any destination with an address.
+	n = NewAnyDest()
+	setID(&n, 2)
+	n.Addr = util.Parse("1.2.3.4")
+	nics = append(nics, n)
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	expected := []tcpip.Route{
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         2,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         2,
+		},
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         1,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         1,
+		},
+	}
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func TestSortByIDAsFallback(t *testing.T) {
+	var nics []netiface.NIC
+
+	// Create NIC 2 for any destination with an address.
+	n := NewAnyDest()
+	setID(&n, 2)
+	n.Addr = util.Parse("1.2.3.4")
+	nics = append(nics, n)
+
+	// Create NIC 1 for any destination with an address.
+	n = NewAnyDest()
+	setID(&n, 1)
+	n.Addr = util.Parse("::1")
+	nics = append(nics, n)
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	expected := []tcpip.Route{
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         1,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         1,
+		},
+		{
+			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 4)),
+			NIC:         2,
+		},
+		{
+			Destination: util.Parse("::"),
+			Mask:        tcpip.AddressMask(strings.Repeat("\x00", 16)),
+			NIC:         2,
+		},
+	}
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func TestSpecificGatewayBeforeAnyGateway(t *testing.T) {
+	nics := []netiface.NIC{
+		{
+			ID: 1,
+			Routes: []tcpip.Route{
+				{
+					Gateway: util.Parse("192.168.42.1"),
+					NIC:     1,
+				},
+				{
+					Gateway: util.Parse("::"),
+					NIC:     1,
+				},
+			},
+		},
+		{
+			ID: 2,
+			Routes: []tcpip.Route{
+				{
+					Gateway: util.Parse("10.0.1.1"),
+					NIC:     2,
+				},
+				{
+					Gateway: util.Parse("::"),
+					NIC:     2,
+				},
+			},
+		},
+	}
+
+	expected := []tcpip.Route{
+		{
+			Gateway: util.Parse("192.168.42.1"),
+			NIC:     1,
+		},
+		{
+			Gateway: util.Parse("10.0.1.1"),
+			NIC:     2,
+		},
+		{
+			Gateway: util.Parse("::"),
+			NIC:     1,
+		},
+		{
+			Gateway: util.Parse("::"),
+			NIC:     2,
+		},
+	}
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func TestSpecificMaskFirst(t *testing.T) {
+	nics := []netiface.NIC{
+		{
+			ID: 1,
+			Routes: []tcpip.Route{
+				{
+					Gateway: util.Parse("192.168.0.1"),
+					Mask:    "\xff\xff\x00\x00",
+					NIC:     1,
+				},
+			},
+		},
+		{
+			ID: 1,
+			Routes: []tcpip.Route{
+				{
+					Gateway: util.Parse("192.168.42.1"),
+					Mask:    "\xff\xff\xff\x00",
+					NIC:     1,
+				},
+			},
+		},
+	}
+
+	expected := []tcpip.Route{
+		{
+			Gateway: util.Parse("192.168.42.1"),
+			Mask:    "\xff\xff\xff\x00",
+			NIC:     1,
+		},
+		{
+			Gateway: util.Parse("192.168.0.1"),
+			Mask:    "\xff\xff\x00\x00",
+			NIC:     1,
+		},
+	}
+
+	var routes []tcpip.Route
+	for _, nic := range nics {
+		routes = append(routes, nic.Routes...)
+	}
+	indexedByID := indexedByID(nics)
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], indexedByID)
+	})
+
+	if diff := cmp.Diff(expected, routes); diff != "" {
+		t.Errorf("(-want +got)\n%s", diff)
+	}
+}
+
+func newV4Address(a, b, c, d uint8) netfidl.IpAddress {
+	return fidlconv.ToNetIpAddress(tcpip.Address(net.IPv4(a, b, c, d).To4()))
+}
+
+var isAnyTests = []struct {
+	addr netfidl.IpAddress
+	res  bool
+}{
+	{
+		addr: newV4Address(0, 0, 0, 0),
+		res:  true,
+	},
+	{
+		addr: newV4Address(127, 0, 0, 1),
+		res:  false,
+	},
+}
+
+func TestIsAny(t *testing.T) {
+	for _, tst := range isAnyTests {
+		if res := netiface.IsAny(fidlconv.ToTCPIPAddress(tst.addr)); res != tst.res {
+			t.Errorf("expected netiface.IsAny(%+v) to be %v, got %v", tst.addr, tst.res, res)
+		}
+	}
+}
diff --git a/garnet/go/src/netstack/netstack.go b/garnet/go/src/netstack/netstack.go
index 41eb268..0e7d94c 100644
--- a/garnet/go/src/netstack/netstack.go
+++ b/garnet/go/src/netstack/netstack.go
@@ -8,6 +8,7 @@
 	"context"
 	"fmt"
 	"log"
+	"sort"
 	"strings"
 	"sync"
 
@@ -19,7 +20,6 @@
 	"netstack/link/eth"
 	"netstack/link/stats"
 	"netstack/netiface"
-	"netstack/routes"
 	"netstack/util"
 
 	"fidl/fuchsia/devicesettings"
@@ -42,18 +42,8 @@
 	deviceSettingsManagerNodenameKey = "DeviceName"
 	defaultNodename                  = "fuchsia-unset-device-name"
 
-	defaultInterfaceMetric routes.Metric = 100
-
-	metricNotSet routes.Metric = 0
-
-	lowPriorityRoute routes.Metric = 99999
-
 	ipv4Loopback tcpip.Address = "\x7f\x00\x00\x01"
 	ipv6Loopback tcpip.Address = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
-
-	// Values used to indicate no IP is assigned to an interface.
-	zeroIpAddr tcpip.Address     = header.IPv4Any
-	zeroIpMask tcpip.AddressMask = "\xff\xff\xff\xff"
 )
 
 // A Netstack tracks all of the running state of the network stack.
@@ -66,7 +56,6 @@
 	mu struct {
 		sync.Mutex
 		stack              *stack.Stack
-		routeTable         routes.RouteTable
 		transactionRequest *netstack.RouteTableTransactionInterfaceRequest
 		countNIC           tcpip.NICID
 		ifStates           map[tcpip.NICID]*ifState
@@ -75,8 +64,7 @@
 
 	filter *filter.Filter
 
-	// This calls both OnInterfacesChanged() and OnInterfacesChanged2() FIDL APIs.
-	OnInterfacesChanged func([]netstack.NetInterface2)
+	OnInterfacesChanged func([]netstack.NetInterface)
 }
 
 // Each ifState tracks the state of a network interface.
@@ -112,8 +100,7 @@
 	bridgeable *bridge.BridgeableEndpoint
 }
 
-// defaultRoutes returns the IPv4 and IPv6 default routes.
-func defaultRoutes(nicid tcpip.NICID, gateway tcpip.Address) []tcpip.Route {
+func defaultRouteTable(nicid tcpip.NICID, gateway tcpip.Address) []tcpip.Route {
 	return []tcpip.Route{
 		{
 			Destination: tcpip.Address(strings.Repeat("\x00", 4)),
@@ -138,114 +125,11 @@
 	}
 }
 
-// AddRoute adds a single route to the route table in a sorted fashion. This
-// takes the lock.
-func (ns *Netstack) AddRoute(r tcpip.Route, metric routes.Metric, dynamic bool) error {
-	log.Printf("adding route %+v metric:%d dynamic=%v", r, metric, dynamic)
-	ns.mu.Lock()
-	defer ns.mu.Unlock()
-	return ns.AddRouteLocked(r, metric, dynamic)
-}
-
-// AddRouteLocked adds a single route to the route table in a sorted fashion. It
-// assumes the lock has already been taken.
-func (ns *Netstack) AddRouteLocked(r tcpip.Route, metric routes.Metric, dynamic bool) error {
-	return ns.AddRoutesLocked([]tcpip.Route{r}, metric, dynamic)
-}
-
-// AddRoutesLocked adds one or more routes to the route table in a sorted
-// fashion. It assumes the lock has already been taken.
-func (ns *Netstack) AddRoutesLocked(rs []tcpip.Route, metric routes.Metric, dynamic bool) error {
-	metricTracksInterface := false
-	if metric == metricNotSet {
-		metricTracksInterface = true
-	}
-
-	for _, r := range rs {
-		// If we don't have an interface set, find it using the gateway address.
-		if r.NIC == 0 {
-			nic, err := ns.mu.routeTable.FindNIC(r.Gateway)
-			if err != nil {
-				return fmt.Errorf("error finding NIC for gateway %v: %s", r.Gateway, err)
-			}
-			r.NIC = nic
-		}
-
-		ifs, ok := ns.mu.ifStates[r.NIC]
-		if !ok {
-			return fmt.Errorf("error getting ifState for NIC %d, not in map", r.NIC)
-		}
-
-		enabled := ifs.mu.state == link.StateStarted
-		if metricTracksInterface {
-			metric = ifs.mu.nic.Metric
-		}
-
-		ns.mu.routeTable.AddRoute(r, metric, metricTracksInterface, dynamic, enabled)
-	}
-	ns.mu.stack.SetRouteTable(ns.mu.routeTable.GetNetstackTable())
-	return nil
-}
-
-// DelRoute deletes a single route from the route table. This takes the lock.
-func (ns *Netstack) DelRoute(r tcpip.Route) error {
-	log.Printf("deleting route %+v", r)
-	ns.mu.Lock()
-	defer ns.mu.Unlock()
-	return ns.DelRouteLocked(r)
-}
-
-// DelRoute deletes a single route from the route table. It assumes the lock has
-// already been taken.
-func (ns *Netstack) DelRouteLocked(r tcpip.Route) error {
-	if err := ns.mu.routeTable.DelRoute(r); err != nil {
-		return fmt.Errorf("error deleting route, %s", err)
-	}
-	ns.mu.stack.SetRouteTable(ns.mu.routeTable.GetNetstackTable())
-	return nil
-}
-
-// GetExtendedRouteTable returns a copy of the current extended route table.
-// This takes the lock.
-func (ns *Netstack) GetExtendedRouteTable() []routes.ExtendedRoute {
-	ns.mu.Lock()
-	defer ns.mu.Unlock()
-	return ns.mu.routeTable.GetExtendedRouteTable()
-}
-
-// UpdateRoutesByInterfaceLocked applies update actions to the routes for a
-// given interface. It assumes the lock has already been taken.
-func (ns *Netstack) UpdateRoutesByInterfaceLocked(nicid tcpip.NICID, action routes.Action) {
-	ns.mu.routeTable.UpdateRoutesByInterface(nicid, action)
-	ns.mu.stack.SetRouteTable(ns.mu.routeTable.GetNetstackTable())
-}
-
-// UpdateInterfaceMetric changes the metric for an interface and updates all
-// routes tracking that interface metric. This takes the lock.
-func (ns *Netstack) UpdateInterfaceMetric(nicid tcpip.NICID, metric routes.Metric) error {
-	log.Printf("update interface metric for NIC %d to metric=%d", nicid, metric)
-
-	ns.mu.Lock()
-	defer ns.mu.Unlock()
-
-	ifState, ok := ns.mu.ifStates[tcpip.NICID(nicid)]
-	if !ok {
-		return fmt.Errorf("error getting ifState for NIC %d, not in map", nicid)
-	}
-	ifState.updateMetric(metric)
-
-	ns.mu.routeTable.UpdateMetricByInterface(nicid, metric)
-	ns.mu.stack.SetRouteTable(ns.mu.routeTable.GetNetstackTable())
-	return nil
-}
-
 func (ns *Netstack) removeInterfaceAddress(nic tcpip.NICID, protocol tcpip.NetworkProtocolNumber, addr tcpip.Address, prefixLen uint8) error {
 	subnet, err := toSubnet(addr, prefixLen)
 	if err != nil {
 		return fmt.Errorf("error parsing subnet format for NIC ID %d: %s", nic, err)
 	}
-	route := subnetRoute(addr, subnet.Mask(), nic)
-	log.Printf("removing static IP %v/%d from NIC %d, deleting subnet route %+v", addr, prefixLen, nic, route)
 
 	ns.mu.Lock()
 	if err := func() error {
@@ -259,35 +143,33 @@
 			return fmt.Errorf("no such subnet %+v for NIC ID %d", subnet, nic)
 		}
 
-		ns.DelRouteLocked(route)
-
 		if err := ns.mu.stack.RemoveAddress(nic, addr); err != nil {
 			return fmt.Errorf("error removing address %s from NIC ID %d: %s", addr, nic, err)
 		}
 
-		newAddr := zeroIpAddr
-		newNetmask := zeroIpMask
-		// Check if the NIC still has other primary IPs and use that one.
-		if mainAddr, subnet, err := ns.mu.stack.GetMainNICAddress(nic, protocol); err == nil {
-			newAddr = mainAddr
-			newNetmask = subnet.Mask()
-			if newNetmask == "" {
-				addressSize := len(newAddr) * 8
-				newNetmask = util.CIDRMask(addressSize, addressSize)
+		if addr, subnet, err := ns.mu.stack.GetMainNICAddress(nic, protocol); err != nil {
+			return fmt.Errorf("error querying NIC ID %d, error: %s", nic, err)
+		} else {
+			netmask := subnet.Mask()
+			if netmask == "" {
+				addressSize := len(addr) * 8
+				netmask = util.CIDRMask(addressSize, addressSize)
+			}
+
+			if ifs, ok := ns.mu.ifStates[nic]; !ok {
+				panic(fmt.Sprintf("Interface state table out of sync: NIC [%d] known to third_party/netstack not found in garnet/netstack", nic))
+			} else {
+				ifs.staticAddressChanged(addr, netmask)
+				ifs.ns.mu.stack.SetRouteTable(ifs.ns.flattenRouteTablesLocked())
 			}
 		}
-		ifs, ok := ns.mu.ifStates[nic]
-		if !ok {
-			panic(fmt.Sprintf("Interface state table out of sync: NIC [%d] known to third_party/netstack not found in garnet/netstack", nic))
-		}
-		ifs.staticAddressChanged(newAddr, newNetmask)
 		return nil
 	}(); err != nil {
 		ns.mu.Unlock()
 		return err
 	}
 
-	interfaces := ns.getNetInterfaces2Locked()
+	interfaces := ns.getNetInterfacesLocked()
 	ns.mu.Unlock()
 	ns.OnInterfacesChanged(interfaces)
 	return nil
@@ -303,8 +185,6 @@
 	if err != nil {
 		return fmt.Errorf("error parsing subnet format for NIC ID %d: %s", nic, err)
 	}
-	route := subnetRoute(addr, subnet.Mask(), nic)
-	log.Printf("adding static IP %v/%d to NIC %d, creating subnet route %+v with metric=<not-set>, dynamic=false", addr, prefixLen, nic, route)
 
 	ns.mu.Lock()
 	if err := func() error {
@@ -320,9 +200,7 @@
 			panic(fmt.Sprintf("Interface state table out of sync: NIC [%d] known to third_party/netstack not found in garnet/netstack", nic))
 		} else {
 			ifs.staticAddressChanged(addr, subnet.Mask())
-			if err := ns.AddRouteLocked(route, metricNotSet, false /* dynamic */); err != nil {
-				return fmt.Errorf("error adding subnet route %v to NIC ID %d: %s", route, nic, err)
-			}
+			ifs.ns.mu.stack.SetRouteTable(ifs.ns.flattenRouteTablesLocked())
 		}
 		return nil
 	}(); err != nil {
@@ -330,22 +208,17 @@
 		return err
 	}
 
-	interfaces := ns.getNetInterfaces2Locked()
+	interfaces := ns.getNetInterfacesLocked()
 	ns.mu.Unlock()
 	ns.OnInterfacesChanged(interfaces)
 	return nil
 }
 
-func (ifs *ifState) updateMetric(metric routes.Metric) {
-	ifs.mu.Lock()
-	ifs.mu.nic.Metric = metric
-	ifs.mu.Unlock()
-}
-
 func (ifs *ifState) staticAddressChanged(newAddr tcpip.Address, netmask tcpip.AddressMask) {
 	ifs.mu.Lock()
 	ifs.mu.nic.Addr = newAddr
 	ifs.mu.nic.Netmask = netmask
+	ifs.mu.nic.Routes = append(ifs.mu.nic.Routes, subnetRoute(newAddr, netmask, ifs.mu.nic.ID))
 	ifs.mu.Unlock()
 }
 
@@ -364,23 +237,18 @@
 	log.Printf("NIC %s: DHCP acquired IP %s for %s", ifs.mu.nic.Name, newAddr, config.LeaseLength)
 	log.Printf("NIC %s: Adding DNS servers: %v", ifs.mu.nic.Name, config.DNS)
 
+	// Update default route with new gateway.
 	ifs.mu.Lock()
+	ifs.mu.nic.Routes = defaultRouteTable(ifs.mu.nic.ID, config.Gateway)
+	ifs.mu.nic.Routes = append(ifs.mu.nic.Routes, subnetRoute(newAddr, config.SubnetMask, ifs.mu.nic.ID))
 	ifs.mu.nic.Netmask = config.SubnetMask
 	ifs.mu.nic.Addr = newAddr
-	ifs.mu.nic.IsDynamicAddr = true
 	ifs.mu.nic.DNSServers = config.DNS
 	ifs.mu.Unlock()
 
-	// Add a default route and a route for the local subnet.
-	rs := defaultRoutes(ifs.mu.nic.ID, config.Gateway)
-	rs = append(rs, subnetRoute(newAddr, config.SubnetMask, ifs.mu.nic.ID))
-	log.Printf("adding routes %+v with metric=<not-set> dynamic=true", rs)
-
 	ifs.ns.mu.Lock()
-	if err := ifs.ns.AddRoutesLocked(rs, metricNotSet, true /* dynamic */); err != nil {
-		log.Printf("error adding routes for DHCP address/gateway: %v", err)
-	}
-	interfaces := ifs.ns.getNetInterfaces2Locked()
+	ifs.ns.mu.stack.SetRouteTable(ifs.ns.flattenRouteTablesLocked())
+	interfaces := ifs.ns.getNetInterfacesLocked()
 	ifs.ns.mu.Unlock()
 
 	ifs.ns.dnsClient.SetRuntimeServers(ifs.ns.getRuntimeDNSServerRefs())
@@ -431,49 +299,54 @@
 		// 	- remove link endpoint
 		//	- reclaim NICID?
 
-		if ifs.mu.nic.IsDynamicAddr || s == link.StateClosed {
-			log.Printf("removing IP from NIC %d", ifs.mu.nic.ID)
-			ifs.mu.nic.Netmask = zeroIpMask
-			ifs.mu.nic.Addr = zeroIpAddr
-			ifs.mu.nic.DNSServers = nil
-		}
-
-		if s == link.StateClosed {
-			// The interface is removed, force all of its routes to be removed.
-			ifs.ns.UpdateRoutesByInterfaceLocked(ifs.mu.nic.ID, routes.ActionDeleteAll)
-		} else {
-			// The interface is down, delete dynamic routes, disable static ones.
-			ifs.ns.UpdateRoutesByInterfaceLocked(ifs.mu.nic.ID, routes.ActionDeleteDynamicDisableStatic)
-		}
+		ifs.mu.nic.Routes = nil
+		ifs.mu.nic.Netmask = "\xff\xff\xff\xff"
+		ifs.mu.nic.Addr = "\x00\x00\x00\x00"
+		ifs.mu.nic.DNSServers = nil
 
 	case link.StateStarted:
 		log.Printf("NIC %s: starting", ifs.mu.nic.Name)
 		ifs.ctx, ifs.cancel = context.WithCancel(context.Background())
-		// Re-enable static routes out this interface.
-		ifs.ns.UpdateRoutesByInterfaceLocked(ifs.mu.nic.ID, routes.ActionEnableStatic)
+		ifs.mu.nic.Routes = defaultRouteTable(ifs.mu.nic.ID, "")
 		if ifs.mu.dhcp.enabled {
 			ifs.mu.dhcp.cancel()
 			ifs.runDHCPLocked()
 		}
-		// TODO(ckuiper): Remove this, as we shouldn't create default routes w/o a
-		// gateway given. Before doing so make sure nothing is still relying on
-		// this.
-		// Update the state before adding the routes, so they are properly enabled.
-		ifs.mu.state = s
-		if err := ifs.ns.AddRoutesLocked(defaultRoutes(ifs.mu.nic.ID, ""), lowPriorityRoute, true /* dynamic */); err != nil {
-			log.Printf("error adding default routes: %v", err)
-		}
 	}
 	ifs.mu.state = s
 	ifs.mu.Unlock()
 
-	interfaces := ifs.ns.getNetInterfaces2Locked()
+	ifs.ns.mu.stack.SetRouteTable(ifs.ns.flattenRouteTablesLocked())
+
+	interfaces := ifs.ns.getNetInterfacesLocked()
 	ifs.ns.mu.Unlock()
 
 	ifs.ns.dnsClient.SetRuntimeServers(ifs.ns.getRuntimeDNSServerRefs())
 	ifs.ns.OnInterfacesChanged(interfaces)
 }
 
+func (ns *Netstack) flattenRouteTablesLocked() []tcpip.Route {
+	routes := make([]tcpip.Route, 0)
+	nics := make(map[tcpip.NICID]*netiface.NIC)
+	for _, ifs := range ns.mu.ifStates {
+		ifs.mu.Lock()
+		routes = append(routes, ifs.mu.nic.Routes...)
+		nics[ifs.mu.nic.ID] = ifs.mu.nic
+		ifs.mu.Unlock()
+	}
+	sort.Slice(routes, func(i, j int) bool {
+		return netiface.Less(&routes[i], &routes[j], nics)
+	})
+	if debug {
+		for i, ifs := range ns.mu.ifStates {
+			log.Printf("[%v] nicid: %v, addr: %v, routes: %v",
+				i, ifs.mu.nic.ID, ifs.mu.nic.Addr, ifs.mu.nic.Routes)
+		}
+	}
+
+	return routes
+}
+
 // Return a slice of references to each NIC's DNS servers.
 // The caller takes ownership of the returned slice.
 func (ns *Netstack) getRuntimeDNSServerRefs() []*[]tcpip.Address {
@@ -551,7 +424,18 @@
 		Addr:     ipv4Loopback,
 		Netmask:  tcpip.AddressMask(strings.Repeat("\xff", len(ipv4Loopback))),
 		Features: ethernet.InfoFeatureLoopback,
-		Metric:   defaultInterfaceMetric,
+		Routes: []tcpip.Route{
+			{
+				Destination: ipv4Loopback,
+				Mask:        tcpip.AddressMask(strings.Repeat("\xff", 4)),
+				NIC:         nicid,
+			},
+			{
+				Destination: ipv6Loopback,
+				Mask:        tcpip.AddressMask(strings.Repeat("\xff", 16)),
+				NIC:         nicid,
+			},
+		},
 	}
 
 	nic.Name = "lo"
@@ -590,24 +474,7 @@
 		return fmt.Errorf("loopback: adding ipv6 address failed: %v", err)
 	}
 
-	err := ns.AddRoutesLocked(
-		[]tcpip.Route{
-			{
-				Destination: ipv4Loopback,
-				Mask:        tcpip.AddressMask(strings.Repeat("\xff", 4)),
-				NIC:         nicid,
-			},
-			{
-				Destination: ipv6Loopback,
-				Mask:        tcpip.AddressMask(strings.Repeat("\xff", 16)),
-				NIC:         nicid,
-			},
-		},
-		metricNotSet, /* use interface metric */
-		false /* dynamic */)
-	if err != nil {
-		return fmt.Errorf("loopback: adding routes failed: %v", err)
-	}
+	ns.mu.stack.SetRouteTable(ns.flattenRouteTablesLocked())
 
 	return nil
 }
@@ -682,9 +549,8 @@
 	}
 	ifs.mu.state = link.StateUnknown
 	ifs.mu.nic = &netiface.NIC{
-		Addr:    zeroIpAddr,
-		Netmask: zeroIpMask,
-		Metric:  defaultInterfaceMetric,
+		Addr:    "\x00\x00\x00\x00",
+		Netmask: "\xff\xff\xff\xff",
 	}
 	ifs.mu.dhcp.running = func() bool { return false }
 	ifs.mu.dhcp.cancel = func() {}
@@ -739,11 +605,14 @@
 
 	ifs.mu.Lock()
 	ifs.mu.nic.ID = nicid
+	ifs.mu.nic.Routes = defaultRouteTable(nicid, "")
 	ifs.mu.nic.Ipv6addrs = []tcpip.Address{lladdr}
 	ifs.statsEP.Nic = ifs.mu.nic
 	ifs.mu.dhcp.Client = dhcp.NewClient(ns.mu.stack, nicid, linkAddr, ifs.dhcpAcquired)
 	ifs.mu.Unlock()
 
+	// Add default route. This will get clobbered later when we get a DHCP response.
+	ns.mu.stack.SetRouteTable(ns.flattenRouteTablesLocked())
 	ns.mu.Unlock()
 
 	return ifs, finalize(ifs)
diff --git a/garnet/go/src/netstack/netstack_service.go b/garnet/go/src/netstack/netstack_service.go
index 0714fc0..27c44f3 100644
--- a/garnet/go/src/netstack/netstack_service.go
+++ b/garnet/go/src/netstack/netstack_service.go
@@ -14,7 +14,6 @@
 
 	"netstack/fidlconv"
 	"netstack/link"
-	"netstack/routes"
 
 	"fidl/fuchsia/hardware/ethernet"
 	"fidl/fuchsia/net"
@@ -38,32 +37,12 @@
 	return out
 }
 
-// interfaces2ListToInterfacesList converts a NetInterface2 list into a
-// NetInterface one.
-func interfaces2ListToInterfacesList(ifs2 []netstack.NetInterface2) []netstack.NetInterface {
-	ifs := make([]netstack.NetInterface, 0, len(ifs2))
-	for _, e2 := range ifs2 {
-		ifs = append(ifs, netstack.NetInterface{
-			Id:        e2.Id,
-			Flags:     e2.Flags,
-			Features:  e2.Features,
-			Name:      e2.Name,
-			Addr:      e2.Addr,
-			Netmask:   e2.Netmask,
-			Broadaddr: e2.Broadaddr,
-			Hwaddr:    e2.Hwaddr,
-			Ipv6addrs: e2.Ipv6addrs,
-		})
-	}
-	return ifs
-}
-
-func (ns *Netstack) getNetInterfaces2Locked() []netstack.NetInterface2 {
+func (ns *Netstack) getNetInterfacesLocked() []netstack.NetInterface {
 	ifStates := ns.mu.ifStates
-	interfaces := make([]netstack.NetInterface2, 0, len(ifStates))
+	interfaces := make([]netstack.NetInterface, 0, len(ifStates))
 	for _, ifs := range ifStates {
 		ifs.mu.Lock()
-		netinterface, err := ifs.toNetInterface2Locked()
+		netinterface, err := ifs.toNetInterfaceLocked()
 		ifs.mu.Unlock()
 		if err != nil {
 			log.Print(err)
@@ -73,11 +52,11 @@
 	return interfaces
 }
 
-func (ifs *ifState) toNetInterface2Locked() (netstack.NetInterface2, error) {
+func (ifs *ifState) toNetInterfaceLocked() (netstack.NetInterface, error) {
 	// Long-hand for: broadaddr = ifs.mu.nic.Addr | ^ifs.mu.nic.Netmask
 	broadaddr := []byte(ifs.mu.nic.Addr)
 	if len(ifs.mu.nic.Netmask) != len(ifs.mu.nic.Addr) {
-		return netstack.NetInterface2{}, fmt.Errorf("address length doesn't match netmask: %+v\n", ifs.mu.nic)
+		return netstack.NetInterface{}, fmt.Errorf("address length doesn't match netmask: %+v\n", ifs.mu.nic)
 	}
 
 	for i := range broadaddr {
@@ -92,11 +71,10 @@
 		flags |= netstack.NetInterfaceFlagDhcp
 	}
 
-	return netstack.NetInterface2{
+	return netstack.NetInterface{
 		Id:        uint32(ifs.mu.nic.ID),
 		Flags:     flags,
 		Features:  ifs.mu.nic.Features,
-		Metric:    uint32(ifs.mu.nic.Metric),
 		Name:      ifs.mu.nic.Name,
 		Addr:      fidlconv.ToNetIpAddress(ifs.mu.nic.Addr),
 		Netmask:   fidlconv.ToNetIpAddress(tcpip.Address(ifs.mu.nic.Netmask)),
@@ -106,8 +84,8 @@
 	}, nil
 }
 
-func (ns *Netstack) getInterfaces2Locked() []netstack.NetInterface2 {
-	out := ns.getNetInterfaces2Locked()
+func (ns *Netstack) getInterfacesLocked() []netstack.NetInterface {
+	out := ns.getNetInterfacesLocked()
 
 	sort.Slice(out, func(i, j int) bool {
 		return out[i].Id < out[j].Id
@@ -148,119 +126,88 @@
 	return out, netstack.NetErr{Status: netstack.StatusOk}, nil
 }
 
-// GetInterfaces2 returns a list of interfaces.
-// TODO(NET-2078): Move this to GetInterfaces once Chromium stops using
-// netstack.fidl.
-func (ni *netstackImpl) GetInterfaces2() ([]netstack.NetInterface2, error) {
-	ni.ns.mu.Lock()
-	defer ni.ns.mu.Unlock()
-	return ni.ns.getInterfaces2Locked(), nil
-}
-
-// GetInterfaces is a deprecated version that returns a list of interfaces in a
-// format that Chromium supports. The new version is GetInterfaces2 which
-// eventually will be renamed once Chromium is not using netstack.fidl anymore
-// and this deprecated version can be removed.
 func (ni *netstackImpl) GetInterfaces() ([]netstack.NetInterface, error) {
 	ni.ns.mu.Lock()
 	defer ni.ns.mu.Unlock()
-	// Get the new interface list and convert to the old one.
-	return interfaces2ListToInterfacesList(ni.ns.getInterfaces2Locked()), nil
+	return ni.ns.getInterfacesLocked(), nil
 }
 
-// TODO(NET-2078): Move this to GetRouteTable once Chromium stops using
-// netstack.fidl.
-func (ni *netstackImpl) GetRouteTable2() ([]netstack.RouteTableEntry2, error) {
-	return nsToRouteTable2(ni.ns.GetExtendedRouteTable()), nil
-}
-
-// GetRouteTable is a deprecated version that returns the route table in a
-// format that Chromium supports. The new version is GetRouteTable2 which will
-// eventually be renamed once Chromium is not using netstack.fidl anymore and
-// this deprecated version can be removed.
 func (ni *netstackImpl) GetRouteTable() ([]netstack.RouteTableEntry, error) {
-	rt2 := nsToRouteTable2(ni.ns.GetExtendedRouteTable())
-	rt := make([]netstack.RouteTableEntry, 0, len(rt2))
-	for _, r2 := range rt2 {
-		var gateway net.IpAddress
-		if r2.Gateway != nil {
-			gateway = *r2.Gateway
-		}
-		rt = append(rt, netstack.RouteTableEntry{
-			Destination: r2.Destination,
-			Netmask:     r2.Netmask,
-			Gateway:     gateway,
-			Nicid:       r2.Nicid,
-		})
-	}
-	return rt, nil
+	ni.ns.mu.Lock()
+	defer ni.ns.mu.Unlock()
+	table := ni.ns.mu.stack.GetRouteTable()
+	return nsToRouteTable(table)
 }
 
-func nsToRouteTable2(table []routes.ExtendedRoute) (out []netstack.RouteTableEntry2) {
-	for _, e := range table {
+func nsToRouteTable(table []tcpip.Route) ([]netstack.RouteTableEntry, error) {
+	out := []netstack.RouteTableEntry{}
+	for _, route := range table {
 		// Ensure that if any of the returned addresses are "empty",
 		// they still have the appropriate length.
 		l := 0
-		if len(e.Route.Destination) > 0 {
-			l = len(e.Route.Destination)
-		} else if len(e.Route.Mask) > 0 {
-			l = len(e.Route.Destination)
+		if len(route.Destination) > 0 {
+			l = len(route.Destination)
+		} else if len(route.Mask) > 0 {
+			l = len(route.Destination)
+		} else if len(route.Gateway) > 0 {
+			l = len(route.Gateway)
 		}
-		dest := e.Route.Destination
-		mask := e.Route.Mask
+		dest := route.Destination
+		mask := route.Mask
+		gateway := route.Gateway
 		if len(dest) == 0 {
 			dest = tcpip.Address(strings.Repeat("\x00", l))
 		}
 		if len(mask) == 0 {
 			mask = tcpip.AddressMask(strings.Repeat("\x00", l))
 		}
-
-		var gatewayPtr *net.IpAddress
-		if len(e.Route.Gateway) != 0 {
-			gateway := fidlconv.ToNetIpAddress(e.Route.Gateway)
-			gatewayPtr = &gateway
+		if len(gateway) == 0 {
+			gateway = tcpip.Address(strings.Repeat("\x00", l))
 		}
-		out = append(out, netstack.RouteTableEntry2{
+
+		out = append(out, netstack.RouteTableEntry{
 			Destination: fidlconv.ToNetIpAddress(dest),
 			Netmask:     fidlconv.ToNetIpAddress(tcpip.Address(mask)),
-			Gateway:     gatewayPtr,
-			Nicid:       uint32(e.Route.NIC),
-			Metric:      uint32(e.Metric),
+			Gateway:     fidlconv.ToNetIpAddress(gateway),
+			Nicid:       uint32(route.NIC),
 		})
 	}
-	return out
+	return out, nil
 }
 
-func routeToNs(r netstack.RouteTableEntry2) tcpip.Route {
-	var gateway tcpip.Address
-	if r.Gateway != nil {
-		gateway = fidlconv.ToTCPIPAddress(*r.Gateway)
+func routeTableToNs(rt []netstack.RouteTableEntry) []tcpip.Route {
+	routes := make([]tcpip.Route, 0, len(rt))
+	for _, r := range rt {
+		routes = append(routes, tcpip.Route{
+			Destination: fidlconv.ToTCPIPAddress(r.Destination),
+			Mask:        tcpip.AddressMask(fidlconv.ToTCPIPAddress(r.Netmask)),
+			Gateway:     fidlconv.ToTCPIPAddress(r.Gateway),
+			NIC:         tcpip.NICID(r.Nicid),
+		})
 	}
-	return tcpip.Route{
-		Destination: fidlconv.ToTCPIPAddress(r.Destination),
-		Mask:        tcpip.AddressMask(fidlconv.ToTCPIPAddress(r.Netmask)),
-		Gateway:     gateway,
-		NIC:         tcpip.NICID(r.Nicid),
-	}
+
+	return routes
 }
 
 type routeTableTransactionImpl struct {
-	ni *netstackImpl
+	ni              *netstackImpl
+	routeTableCache []tcpip.Route
 }
 
-func (i *routeTableTransactionImpl) AddRoute(r netstack.RouteTableEntry2) (int32, error) {
-	err := i.ni.ns.AddRoute(routeToNs(r), routes.Metric(r.Metric), false /* not dynamic */)
-	if err != nil {
-		return int32(zx.ErrInvalidArgs), err
-	}
-	return int32(zx.ErrOk), nil
+func (i *routeTableTransactionImpl) GetRouteTable() ([]netstack.RouteTableEntry, error) {
+	return nsToRouteTable(i.routeTableCache)
 }
 
-func (i *routeTableTransactionImpl) DelRoute(r netstack.RouteTableEntry2) (int32, error) {
-	err := i.ni.ns.DelRoute(routeToNs(r))
-	if err != nil {
-		return int32(zx.ErrInvalidArgs), err
-	}
+func (i *routeTableTransactionImpl) SetRouteTable(rt []netstack.RouteTableEntry) error {
+	routes := routeTableToNs(rt)
+	i.routeTableCache = routes
+	return nil
+}
+
+func (i *routeTableTransactionImpl) Commit() (int32, error) {
+	i.ni.ns.mu.Lock()
+	defer i.ni.ns.mu.Unlock()
+	i.ni.ns.mu.stack.SetRouteTable(i.routeTableCache)
 	return int32(zx.ErrOk), nil
 }
 
@@ -287,7 +234,10 @@
 		ni.ns.mu.transactionRequest = &req
 	}
 	var routeTableService netstack.RouteTableTransactionService
-	transaction := routeTableTransactionImpl{ni: ni}
+	transaction := routeTableTransactionImpl{
+		ni:              ni,
+		routeTableCache: ni.ns.mu.stack.GetRouteTable(),
+	}
 	// We don't use the error handler to free the channel because it's
 	// possible that the peer closes the channel before our service has
 	// finished processing.
@@ -330,14 +280,6 @@
 	return netstack.NetErr{Status: netstack.StatusOk, Message: ""}, nil
 }
 
-// SetInterfaceMetric updates the metric of an interface.
-func (ni *netstackImpl) SetInterfaceMetric(nicid uint32, metric uint32) (result netstack.NetErr, err error) {
-	if err := ni.ns.UpdateInterfaceMetric(tcpip.NICID(nicid), routes.Metric(metric)); err != nil {
-		return netstack.NetErr{Status: netstack.StatusUnknownInterface, Message: err.Error()}, nil
-	}
-	return netstack.NetErr{Status: netstack.StatusOk}, nil
-}
-
 func (ni *netstackImpl) BridgeInterfaces(nicids []uint32) (netstack.NetErr, error) {
 	nics := make([]tcpip.NICID, len(nicids))
 	for i, n := range nicids {
diff --git a/garnet/go/src/netstack/netstack_service_impl_test.go b/garnet/go/src/netstack/netstack_service_impl_test.go
index 45b2a1c..1f0abb5 100644
--- a/garnet/go/src/netstack/netstack_service_impl_test.go
+++ b/garnet/go/src/netstack/netstack_service_impl_test.go
@@ -11,7 +11,6 @@
 	"syscall/zx/fidl"
 	"testing"
 
-	"fidl/fuchsia/hardware/ethernet"
 	netfidl "fidl/fuchsia/net"
 	"fidl/fuchsia/netstack"
 
@@ -46,19 +45,7 @@
 func TestRouteTableTransactions(t *testing.T) {
 	go fidl.Serve()
 	t.Run("no contentions", func(t *testing.T) {
-		// Create a basic netstack instance with a single interface. We need at
-		// least one interface in order to add routes.
-		netstackServiceImpl := netstackImpl{ns: newNetstack(t)}
-		eth := deviceForAddEth(ethernet.Info{}, t)
-		ifs, err := netstackServiceImpl.ns.addEth("/fake/ethernet/device", netstack.InterfaceConfig{Name: "testdevice"}, &eth)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		var originalTable []netstack.RouteTableEntry2
-		originalTable, err = netstackServiceImpl.GetRouteTable2()
-		AssertNoError(t, err)
-
+		netstackServiceImpl := MakeNetstackService()
 		req, transactionInterface, err := netstack.NewRouteTableTransactionInterfaceRequest()
 		AssertNoError(t, err)
 
@@ -70,67 +57,29 @@
 			t.Errorf("can't start a transaction")
 		}
 
+		rs, err := transactionInterface.GetRouteTable()
+		AssertNoError(t, err)
+
 		destinationAddress, destinationSubnet, err := net.ParseCIDR("1.2.3.4/24")
 		AssertNoError(t, err)
-		gatewayAddress := net.ParseIP("5.6.7.8")
-		if gatewayAddress == nil {
-			t.Fatal("Cannot create gateway IP")
-		}
-		gateway := toIpAddress(gatewayAddress)
-		newRouteTableEntry2 := netstack.RouteTableEntry2{
+		gatewayAddress, _, err := net.ParseCIDR("5.6.7.8/32")
+		newRouteTableEntry := netstack.RouteTableEntry{
 			Destination: toIpAddress(destinationAddress),
 			Netmask:     toIpAddress(net.IP(destinationSubnet.Mask)),
-			Gateway:     &gateway,
-			Nicid:       uint32(ifs.mu.nic.ID),
-			Metric:      100,
+			Gateway:     toIpAddress(gatewayAddress),
 		}
 
-		success, err = transactionInterface.AddRoute(newRouteTableEntry2)
-		AssertNoError(t, err)
-		if zx.Status(success) != zx.ErrOk {
-			t.Fatal("can't add new route entry")
-		}
+		newRouteTable := append(rs, newRouteTableEntry)
+		AssertNoError(t, transactionInterface.SetRouteTable(newRouteTable))
 
-		// New table should contain the one route we just added.
-		actualTable2, err := netstackServiceImpl.GetRouteTable2()
+		_, err = transactionInterface.Commit()
 		AssertNoError(t, err)
-		if diff := cmp.Diff(actualTable2[0], newRouteTableEntry2, cmpopts.IgnoreTypes(struct{}{})); diff != "" {
+
+		actual, err := netstackServiceImpl.GetRouteTable()
+		AssertNoError(t, err)
+		if diff := cmp.Diff(actual, newRouteTable, cmpopts.IgnoreTypes(struct{}{})); diff != "" {
 			t.Errorf("(-want +got)\n%s", diff)
 		}
-
-		// Verify deprecated GetRouteTable() function returns equal entries.
-		expectedRouteTableEntry := netstack.RouteTableEntry{
-			Destination: newRouteTableEntry2.Destination,
-			Netmask:     newRouteTableEntry2.Netmask,
-			Gateway:     *newRouteTableEntry2.Gateway,
-			Nicid:       newRouteTableEntry2.Nicid,
-			// no metric
-		}
-		actualTable, err := netstackServiceImpl.GetRouteTable()
-		AssertNoError(t, err)
-		if diff := cmp.Diff(actualTable[0], expectedRouteTableEntry, cmpopts.IgnoreTypes(struct{}{})); diff != "" {
-			t.Errorf("(-want +got)\n%s", diff)
-		}
-
-		success, err = transactionInterface.DelRoute(newRouteTableEntry2)
-		AssertNoError(t, err)
-		if zx.Status(success) != zx.ErrOk {
-			t.Error("can't delete route entry")
-		}
-
-		// New table should be empty.
-		actualTable2, err = netstackServiceImpl.GetRouteTable2()
-		AssertNoError(t, err)
-		if len(actualTable2) != len(originalTable) {
-			t.Errorf("got %v, want <nothing>", actualTable2)
-		}
-
-		// Same for deprecated route table.
-		actualTable, err = netstackServiceImpl.GetRouteTable()
-		AssertNoError(t, err)
-		if len(actualTable) != len(originalTable) {
-			t.Errorf("got %v, want <nothing>", actualTable)
-		}
 	})
 
 	t.Run("contentions", func(t *testing.T) {
diff --git a/garnet/go/src/netstack/netstack_test.go b/garnet/go/src/netstack/netstack_test.go
index 847085b..7ee77ff 100644
--- a/garnet/go/src/netstack/netstack_test.go
+++ b/garnet/go/src/netstack/netstack_test.go
@@ -190,7 +190,7 @@
 			arp.ProtocolName,
 		}, nil, stack.Options{})
 
-	ns.OnInterfacesChanged = func([]netstack.NetInterface2) {}
+	ns.OnInterfacesChanged = func([]netstack.NetInterface) {}
 	ipAddressConfig := netstack.IpAddressConfig{}
 	addr := fidlconv.ToNetIpAddress(testIpAddress)
 	subnet := net.Subnet{Addr: addr, PrefixLen: 32}
@@ -222,7 +222,7 @@
 			arp.ProtocolName,
 		}, nil, stack.Options{})
 
-	ns.OnInterfacesChanged = func([]netstack.NetInterface2) {}
+	ns.OnInterfacesChanged = func([]netstack.NetInterface) {}
 	return ns
 }
 
diff --git a/garnet/go/src/netstack/netstat/netstat.go b/garnet/go/src/netstack/netstat/netstat.go
index 76d0eab..0d43c545 100644
--- a/garnet/go/src/netstack/netstat/netstat.go
+++ b/garnet/go/src/netstack/netstat/netstat.go
@@ -118,7 +118,7 @@
 }
 
 func dumpStats(a *netstatApp) {
-	nics, err := a.netstack.GetInterfaces2()
+	nics, err := a.netstack.GetInterfaces()
 	if err != nil {
 		errorf("Failed to get interfaces: %v\n.", err)
 		return
@@ -141,26 +141,22 @@
 }
 
 func dumpRouteTables(a *netstatApp) {
-	entries, err := a.netstack.GetRouteTable2()
+	entries, err := a.netstack.GetRouteTable()
 	if err != nil {
 		errorf("Failed to get route table: %v\n", err)
 		return
 	}
 	for _, entry := range entries {
 		if netAddressZero(entry.Destination) {
-			if entry.Gateway != nil && !netAddressZero(*entry.Gateway) {
-				fmt.Printf("default via %s, ", netAddressToString(*entry.Gateway))
-			} else {
-				fmt.Printf("default through ")
-			}
+			fmt.Printf("default via %s, ", netAddressToString(entry.Gateway))
 		} else {
 			fmt.Printf("Destination: %s, ", netAddressToString(entry.Destination))
 			fmt.Printf("Mask: %s, ", netAddressToString(entry.Netmask))
-			if entry.Gateway != nil && !netAddressZero(*entry.Gateway) {
-				fmt.Printf("Gateway: %s, ", netAddressToString(*entry.Gateway))
+			if !netAddressZero(entry.Gateway) {
+				fmt.Printf("Gateway: %s, ", netAddressToString(entry.Gateway))
 			}
 		}
-		fmt.Printf("NICID: %d Metric: %v\n", entry.Nicid, entry.Metric)
+		fmt.Printf("NICID: %d\n", entry.Nicid)
 	}
 }
 
diff --git a/garnet/go/src/netstack/routes/BUILD.gn b/garnet/go/src/netstack/routes/BUILD.gn
deleted file mode 100644
index dde999b1..0000000
--- a/garnet/go/src/netstack/routes/BUILD.gn
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2018 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.
-
-import("//build/go/go_test.gni")
-
-go_test("netstack_routes_test") {
-  gopackage = "netstack/routes"
-
-  deps = [
-    "//garnet/go/src/netstack:netstack_lib",
-  ]
-}
diff --git a/garnet/go/src/netstack/routes/routes.go b/garnet/go/src/netstack/routes/routes.go
deleted file mode 100644
index 8d42566..0000000
--- a/garnet/go/src/netstack/routes/routes.go
+++ /dev/null
@@ -1,390 +0,0 @@
-// 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.
-
-package routes
-
-import (
-	"fmt"
-	"log"
-	"sort"
-	"strings"
-	"sync"
-
-	"netstack/util"
-
-	"github.com/google/netstack/tcpip"
-	"github.com/google/netstack/tcpip/header"
-)
-
-const debug = false
-
-type Action uint32
-
-const (
-	ActionDeleteAll Action = iota
-	ActionDeleteDynamicDisableStatic
-	ActionEnableStatic
-)
-
-// Metric is the metric used for sorting the route table. It acts as a
-// priority with a lower value being better.
-type Metric uint32
-
-// ExtendedRoute is a single route that contains the standard tcpip.Route plus
-// additional attributes.
-type ExtendedRoute struct {
-	// Route used to build the route table to be fed into the
-	// github.com/google/netstack lib.
-	Route tcpip.Route
-
-	// Metric acts as a tie-breaker when comparing otherwise identical routes.
-	Metric Metric
-
-	// MetricTracksInterface is true when the metric tracks the metric of the
-	// interface for this route. This means when the interface metric changes, so
-	// will this route's metric. If false, the metric is static and only changed
-	// explicitly by API.
-	MetricTracksInterface bool
-
-	// Dynamic marks a route as being obtained via DHCP. Such routes are removed
-	// from the table when the interface goes down, vs. just being disabled.
-	Dynamic bool
-
-	// Enabled marks a route as inactive, i.e., its interface is down and packets
-	// must not use this route.
-	// Disabled routes are omitted when building the route table for the
-	// Netstack lib.
-	// This flag is used with non-dynamic routes (i.e., statically added routes)
-	// to keep them in the table while their interface is down.
-	Enabled bool
-}
-
-// Match matches the given address against this route.
-func (er *ExtendedRoute) Match(addr tcpip.Address) bool {
-	r := er.Route
-	if len(addr) != len(r.Destination) {
-		return false
-	}
-	for i := 0; i < len(r.Destination); i++ {
-		if (addr[i] & r.Mask[i]) != r.Destination[i] {
-			return false
-		}
-	}
-	return true
-}
-
-func (er *ExtendedRoute) String() string {
-	var out strings.Builder
-	out.WriteString(fmt.Sprintf("  %v/%v", er.Route.Destination, util.PrefixLength(er.Route.Mask)))
-	if len(er.Route.Gateway) > 0 {
-		out.WriteString(fmt.Sprintf(" via %v", er.Route.Gateway))
-	}
-	out.WriteString(fmt.Sprintf(" nic %v", er.Route.NIC))
-	if er.MetricTracksInterface {
-		out.WriteString(fmt.Sprintf(" metric[if] %v", er.Metric))
-	} else {
-		out.WriteString(fmt.Sprintf(" metric[static] %v", er.Metric))
-	}
-	if er.Dynamic {
-		out.WriteString(" (dynamic)")
-	} else {
-		out.WriteString(" (static)")
-	}
-	if !er.Enabled {
-		out.WriteString(" (disabled)")
-	}
-	out.WriteString("\n")
-	return out.String()
-}
-
-// RouteTable implements a sorted list of extended routes that is used to build
-// the Netstack lib route table.
-type RouteTable struct {
-	mu struct {
-		sync.Mutex
-		routes []ExtendedRoute
-	}
-}
-
-func (rt *RouteTable) String() string {
-	var out strings.Builder
-	for _, r := range rt.mu.routes {
-		out.WriteString(fmt.Sprintf("%v", &r))
-	}
-	return out.String()
-}
-
-// For debugging.
-func (rt *RouteTable) Dump() {
-	fmt.Printf("Current Route Table:\n%v", rt)
-}
-
-// For testing.
-func (rt *RouteTable) Set(r []ExtendedRoute) {
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-	rt.mu.routes = append([]ExtendedRoute(nil), r...)
-}
-
-// AddRoute inserts the given route to the table in a sorted fashion. If the
-// route already exists, it simply updates that route's metric, dynamic and
-// enabled fields.
-func (rt *RouteTable) AddRoute(route tcpip.Route, metric Metric, tracksInterface bool, dynamic bool, enabled bool) {
-	if debug {
-		log.Printf("RouteTable:Adding route %+v with metric:%d, trackIf=%v, dynamic=%v, enabled=%v", route, metric, tracksInterface, dynamic, enabled)
-	}
-
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	// First check if the route already exists, and remove it.
-	for i, er := range rt.mu.routes {
-		if IsSameRoute(route, er.Route) {
-			rt.mu.routes = append(rt.mu.routes[:i], rt.mu.routes[i+1:]...)
-			break
-		}
-	}
-
-	newEr := ExtendedRoute{
-		Route:                 route,
-		Metric:                metric,
-		MetricTracksInterface: tracksInterface,
-		Dynamic:               dynamic,
-		Enabled:               enabled,
-	}
-
-	// Find the target position for the new route in the table so it remains
-	// sorted. Initialized to point to the end of the table.
-	targetIdx := len(rt.mu.routes)
-	for i, er := range rt.mu.routes {
-		if Less(&newEr, &er) {
-			targetIdx = i
-			break
-		}
-	}
-	// Extend the table by adding the new route at the end, then move it into its
-	// proper place.
-	rt.mu.routes = append(rt.mu.routes, newEr)
-	if targetIdx < len(rt.mu.routes)-1 {
-		copy(rt.mu.routes[targetIdx+1:], rt.mu.routes[targetIdx:])
-		rt.mu.routes[targetIdx] = newEr
-	}
-
-	if debug {
-		rt.Dump()
-	}
-}
-
-// DelRoute removes the given route from the route table.
-func (rt *RouteTable) DelRoute(route tcpip.Route) error {
-	if debug {
-		log.Printf("RouteTable:Deleting route %+v", route)
-	}
-
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	routeDeleted := false
-	oldTable := rt.mu.routes
-	rt.mu.routes = oldTable[:0]
-	for _, er := range oldTable {
-		// Match all fields that are non-zero.
-		if isSameSubnet(er.Route, route) {
-			if route.NIC == 0 || route.NIC == er.Route.NIC {
-				if len(route.Gateway) == 0 || route.Gateway == er.Route.Gateway {
-					routeDeleted = true
-					continue
-				}
-			}
-		}
-		// Not matched, remains in the route table.
-		rt.mu.routes = append(rt.mu.routes, er)
-	}
-
-	if !routeDeleted {
-		return fmt.Errorf("no such route")
-	}
-
-	if debug {
-		rt.Dump()
-	}
-	return nil
-}
-
-// GetExtendedRouteTable returns a copy of the current extended route table.
-func (rt *RouteTable) GetExtendedRouteTable() []ExtendedRoute {
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	if debug {
-		rt.Dump()
-	}
-
-	return append([]ExtendedRoute(nil), rt.mu.routes...)
-}
-
-// GetNetstackTable returns the route table to be fed into the
-// github.com/google/netstack lib. It contains all routes except for disabled
-// ones.
-func (rt *RouteTable) GetNetstackTable() []tcpip.Route {
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	t := make([]tcpip.Route, 0, len(rt.mu.routes))
-	for _, er := range rt.mu.routes {
-		if er.Enabled {
-			t = append(t, er.Route)
-		}
-	}
-
-	if debug {
-		log.Printf("RouteTable:Netstack route table: %+v", t)
-	}
-
-	return t
-}
-
-// UpdateMetricByInterface changes the metric for all routes that track a
-// given interface.
-func (rt *RouteTable) UpdateMetricByInterface(nicid tcpip.NICID, metric Metric) {
-	if debug {
-		log.Printf("RouteTable:Update route table on nic-%d metric change to %d", nicid, metric)
-	}
-
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	for i, er := range rt.mu.routes {
-		if er.Route.NIC == nicid && er.MetricTracksInterface {
-			rt.mu.routes[i].Metric = metric
-		}
-	}
-
-	rt.sortRouteTableLocked()
-
-	if debug {
-		rt.Dump()
-	}
-}
-
-// UpdateRoutesByInterface applies an action to the routes pointing to an interface.
-func (rt *RouteTable) UpdateRoutesByInterface(nicid tcpip.NICID, action Action) {
-	if debug {
-		log.Printf("RouteTable:Update route table for routes to nic-%d with action:%d", nicid, action)
-	}
-
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	oldTable := rt.mu.routes
-	rt.mu.routes = oldTable[:0]
-	for _, er := range oldTable {
-		if er.Route.NIC == nicid {
-			switch action {
-			case ActionDeleteAll:
-				continue // delete
-			case ActionDeleteDynamicDisableStatic:
-				if er.Dynamic {
-					continue // delete
-				}
-				er.Enabled = false
-			case ActionEnableStatic:
-				if !er.Dynamic {
-					er.Enabled = true
-				}
-			}
-		}
-		// Keep.
-		rt.mu.routes = append(rt.mu.routes, er)
-	}
-
-	rt.sortRouteTableLocked()
-
-	if debug {
-		rt.Dump()
-	}
-}
-
-// FindNIC returns the NIC-ID that the given address is routed on. This requires
-// an exact route match, i.e. no default route.
-func (rt *RouteTable) FindNIC(addr tcpip.Address) (tcpip.NICID, error) {
-	rt.mu.Lock()
-	defer rt.mu.Unlock()
-
-	for _, er := range rt.mu.routes {
-		// Ignore default routes.
-		if util.IsAny(er.Route.Destination) {
-			continue
-		}
-		if er.Match(addr) && er.Route.NIC > 0 {
-			return er.Route.NIC, nil
-		}
-	}
-	return 0, fmt.Errorf("cannot find NIC with valid destination route to %v", addr)
-}
-
-func (rt *RouteTable) sortRouteTableLocked() {
-	sort.SliceStable(rt.mu.routes, func(i, j int) bool {
-		return Less(&rt.mu.routes[i], &rt.mu.routes[j])
-	})
-}
-
-// Less compares two routes and returns which one should appear earlier in the
-// route table.
-func Less(ei, ej *ExtendedRoute) bool {
-	ri, rj := ei.Route, ej.Route
-	// Non-default before default one.
-	if util.IsAny(ri.Destination) != util.IsAny(rj.Destination) {
-		return !util.IsAny(ri.Destination)
-	}
-
-	// IPv4 before IPv6 (arbitrary choice).
-	if len(ri.Destination) != len(rj.Destination) {
-		return len(ri.Destination) == header.IPv4AddressSize
-	}
-
-	// Longer prefix wins.
-	li, lj := util.PrefixLength(ri.Mask), util.PrefixLength(rj.Mask)
-	if len(ri.Mask) == len(rj.Mask) && li != lj {
-		return li > lj
-	}
-
-	// Lower metrics wins.
-	if ei.Metric != ej.Metric {
-		return ei.Metric < ej.Metric
-	}
-
-	// Everything that matters is the same. At this point we still need a
-	// deterministic way to tie-break. First go by destination IPs (lower wins),
-	// finally use the NIC.
-	riDest, rjDest := []byte(ri.Destination), []byte(rj.Destination)
-	for i := 0; i < len(riDest); i++ {
-		if riDest[i] != rjDest[i] {
-			return riDest[i] < rjDest[i]
-		}
-	}
-
-	// Same prefix and destination IPs (e.g. loopback IPs), use NIC as final
-	// tie-breaker.
-	return ri.NIC < rj.NIC
-}
-
-func isSameSubnet(a, b tcpip.Route) bool {
-	return a.Destination == b.Destination && a.Mask == b.Mask
-}
-
-// IsSameRoute returns true if two routes are the same.
-func IsSameRoute(a, b tcpip.Route) bool {
-	if !isSameSubnet(a, b) || a.NIC != b.NIC {
-		return false
-	}
-
-	aHasGW := len(a.Gateway) > 0 && !util.IsAny(a.Gateway)
-	bHasGW := len(a.Gateway) > 0 && !util.IsAny(b.Gateway)
-	if aHasGW && bHasGW {
-		return a.Gateway == b.Gateway
-	}
-	// either one or both routes have no gateway
-	return !aHasGW && !bHasGW
-}
diff --git a/garnet/go/src/netstack/routes/routes_test.go b/garnet/go/src/netstack/routes/routes_test.go
deleted file mode 100644
index efae8e1..0000000
--- a/garnet/go/src/netstack/routes/routes_test.go
+++ /dev/null
@@ -1,602 +0,0 @@
-// Copyright 2017 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package routes_test
-
-import (
-	"fmt"
-	"net"
-	"testing"
-
-	"netstack/routes"
-	"netstack/util"
-
-	"github.com/google/netstack/tcpip"
-)
-
-var testRouteTable = []routes.ExtendedRoute{
-	// nic, subnet, gateway, metric, tracksInterface, dynamic, enabled
-	createExtendedRoute(1, "127.0.0.1/32", "", 100, true, false, true),                 // loopback
-	createExtendedRoute(4, "192.168.1.0/24", "", 100, true, true, true),                // DHCP IP (eth)
-	createExtendedRoute(2, "192.168.100.0/24", "", 200, true, true, true),              // DHCP IP (wlan)
-	createExtendedRoute(3, "10.1.2.0/23", "", 100, true, false, true),                  // static IP
-	createExtendedRoute(2, "110.34.0.0/16", "192.168.100.10", 500, false, false, true), // static route
-	createExtendedRoute(1, "::1/128", "", 100, true, false, true),                      // loopback
-	// static route (eth)
-	createExtendedRoute(4, "2610:1:22:123:34:faed::/96", "fe80::250:a8ff:9:79", 100, false, false, true),
-	createExtendedRoute(4, "2622:0:2200:10::/64", "", 100, true, true, true),           // RA (eth)
-	createExtendedRoute(4, "0.0.0.0/0", "192.168.1.1", 100, true, true, true),          // default (eth)
-	createExtendedRoute(2, "0.0.0.0/0", "192.168.100.10", 200, true, true, true),       // default (wlan)
-	createExtendedRoute(4, "::/0", "fe80::210:6e00:fe11:3265", 100, true, false, true), // default (eth)
-}
-
-func createRoute(nicid tcpip.NICID, subnet string, gateway string) tcpip.Route {
-	_, s, _ := net.ParseCIDR(subnet)
-	return tcpip.Route{
-		Destination: tcpip.Address(s.IP),
-		Mask:        tcpip.AddressMask(s.Mask),
-		Gateway:     ipStringToAddress(gateway),
-		NIC:         nicid,
-	}
-}
-
-func createExtendedRoute(nicid tcpip.NICID, subnet string, gateway string, metric routes.Metric, tracksInterface bool, dynamic bool, enabled bool) routes.ExtendedRoute {
-	return routes.ExtendedRoute{
-		Route:                 createRoute(nicid, subnet, gateway),
-		Metric:                metric,
-		MetricTracksInterface: tracksInterface,
-		Dynamic:               dynamic,
-		Enabled:               enabled,
-	}
-}
-
-func ipStringToAddress(ipStr string) tcpip.Address {
-	return ipToAddress(net.ParseIP(ipStr))
-}
-
-func ipToAddress(ip net.IP) tcpip.Address {
-	if v4 := ip.To4(); v4 != nil {
-		return tcpip.Address(v4)
-	}
-	return tcpip.Address(ip)
-}
-
-func TestExtendedRouteMatch(t *testing.T) {
-	for _, tc := range []struct {
-		subnet string
-		addr   string
-		want   bool
-	}{
-		{"192.168.10.0/24", "192.168.10.0", true},
-		{"192.168.10.0/24", "192.168.10.1", true},
-		{"192.168.10.0/24", "192.168.10.10", true},
-		{"192.168.10.0/24", "192.168.10.255", true},
-		{"192.168.10.0/24", "192.168.11.1", false},
-		{"192.168.10.0/24", "192.168.0.1", false},
-		{"192.168.10.0/24", "192.167.10.1", false},
-		{"192.168.10.0/24", "193.168.10.1", false},
-		{"192.168.10.0/24", "0.0.0.0", false},
-
-		{"123.220.0.0/14", "123.220.11.22", true},
-		{"123.220.0.0/14", "123.221.11.22", true},
-		{"123.220.0.0/14", "123.222.11.22", true},
-		{"123.220.0.0/14", "123.223.11.22", true},
-		{"123.220.0.0/14", "123.224.11.22", false},
-
-		{"0.0.0.0/0", "0.0.0.0", true},
-		{"0.0.0.0/0", "1.1.1.1", true},
-		{"0.0.0.0/0", "255.255.255.255", true},
-
-		{"2402:f000:5:8401::/64", "2402:f000:5:8401::", true},
-		{"2402:f000:5:8401::/64", "2402:f000:5:8401::1", true},
-		{"2402:f000:5:8401::/64", "2402:f000:5:8401:1:12:123::", true},
-		{"2402:f000:5:8401::/64", "2402:f000:5:8402::", false},
-		{"2402:f000:5:8401::/64", "2402:f000:15:8401::", false},
-		{"2402:f000:5:8401::/64", "2402::5:8401::", false},
-		{"2402:f000:5:8401::/64", "2400:f000:5:8401::", false},
-
-		{"::/0", "::", true},
-		{"::/0", "1:1:1:1:1:1:1:1", true},
-		{"::/0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
-	} {
-		t.Run("RouteMatch", func(t *testing.T) {
-			er := createExtendedRoute(1, tc.subnet, "", 100, true, true, true)
-			addr := ipStringToAddress(tc.addr)
-			if got := er.Match(addr); got != tc.want {
-				t.Errorf("got match addr %v in subnet %v = %v, want = %v", tc.addr, tc.subnet, got, tc.want)
-			}
-		})
-	}
-}
-
-func TestSortingLess(t *testing.T) {
-	for _, tc := range []struct {
-		subnet1 string
-		metric1 routes.Metric
-		nic1    tcpip.NICID
-
-		subnet2 string
-		metric2 routes.Metric
-		nic2    tcpip.NICID
-
-		want bool
-	}{
-		// non-default before default
-		{"100.99.24.12/32", 100, 1, "0.0.0.0/0", 100, 1, true},
-		{"10.144.0.0/12", 100, 1, "0.0.0.0/0", 100, 2, true},
-		{"127.0.0.1/24", 100, 1, "0.0.0.0/0", 100, 1, true},
-		{"2511:5f32:4:6:124::2:1/128", 100, 1, "::/0", 100, 1, true},
-		{"fe80:ffff:0:1234:5:6::/96", 100, 2, "::/0", 100, 1, true},
-		{"::1/128", 100, 2, "::/0", 100, 2, true},
-		// IPv4 before IPv6
-		{"100.99.24.12/32", 100, 1, "2511:5f32:4:6:124::2/120", 100, 1, true},
-		{"100.99.24.12/32", 100, 2, "2605:32::12/128", 100, 1, true},
-		{"127.0.0.1/24", 100, 1, "::1/128", 100, 1, true},
-		{"0.0.0.0/0", 100, 1, "::/0", 100, 3, true},
-		// longer prefix wins
-		{"100.99.24.12/32", 100, 2, "100.99.24.12/31", 100, 1, true},
-		{"100.99.24.12/32", 100, 1, "10.144.0.0/12", 100, 1, true},
-		{"100.99.24.128/25", 100, 5, "127.0.0.1/24", 100, 3, true},
-		{"2511:5f32:4:6:124::2:12/128", 100, 1, "2511:5f32:4:6:124::2:12/126", 100, 1, true},
-		{"2511:5f32:4:6:124::2:12/128", 100, 1, "2605:32::12/127", 100, 1, true},
-		{"fe80:ffff:0:1234:5:6::/96", 100, 2, "2511:5f32:4:6:124::/90", 100, 1, true},
-		{"2511:5f32:4:6:124::2:1/128", 100, 1, "::1/120", 100, 2, true},
-		// lower metric
-		{"100.99.24.12/32", 100, 1, "10.1.21.31/32", 101, 1, true},
-		{"100.99.24.12/32", 101, 1, "10.1.21.31/32", 100, 2, false},
-		{"100.99.2.0/23", 100, 3, "10.1.22.0/23", 101, 1, true},
-		{"100.99.2.0/23", 101, 1, "10.1.22.0/23", 100, 1, false},
-		{"2511:5f32:4:6:124::2:1/128", 100, 1, "2605:32::12/128", 101, 1, true},
-		{"2511:5f32:4:6:124::2:1/128", 101, 3, "2605:32::12/128", 100, 2, false},
-		{"2511:5f32:4:6:124::2:1/96", 100, 1, "fe80:ffff:0:1234:5:6::/96", 101, 4, true},
-		{"2511:5f32:4:6:124::2:1/96", 101, 1, "fe80:ffff:0:1234:5:6::/96", 100, 4, false},
-		// tie-breaker: destination IPs
-		{"10.1.21.31/32", 100, 2, "100.99.24.12/32", 100, 1, true},
-		{"100.99.24.12/32", 100, 1, "10.1.21.31/32", 100, 3, false},
-		{"2511:5f32:4:6:124::2:1/128", 100, 1, "2605:32::12/128", 100, 1, true},
-		{"2605:32::12/128", 100, 1, "2511:5f32:4:6:124::2:1/128", 100, 1, false},
-		// tie-breaker: NIC
-		{"10.1.21.31/32", 100, 1, "10.1.21.31/32", 100, 2, true},
-		{"10.1.21.31/32", 100, 2, "10.1.21.31/32", 100, 1, false},
-		{"2511:5f32:4:6:124::2:1/128", 100, 1, "2511:5f32:4:6:124::2:1/128", 100, 2, true},
-		{"2511:5f32:4:6:124::2:1/128", 100, 2, "2511:5f32:4:6:124::2:1/128", 100, 1, false},
-	} {
-		name := fmt.Sprintf("Test-%s@nic%v[m:%v]_<_%s@nic%v[m:%v]", tc.subnet1, tc.nic1, tc.metric1, tc.subnet2, tc.nic2, tc.metric2)
-		t.Run(name, func(t *testing.T) {
-			e1 := createExtendedRoute(tc.nic1, tc.subnet1, "", tc.metric1, true, true, true)
-			e2 := createExtendedRoute(tc.nic2, tc.subnet2, "", tc.metric2, true, true, true)
-			if got := routes.Less(&e1, &e2); got != tc.want {
-				t.Errorf("got Less(%v, %v) = %v, want = %v", &e1, &e2, got, tc.want)
-			}
-			// reverse test
-			reverseWant := !tc.want
-			if got := routes.Less(&e2, &e1); got != reverseWant {
-				t.Errorf("Less(%v, %v) = %v, want = %v", e2, e1, got, reverseWant)
-			}
-		})
-	}
-}
-
-func changeByte(b []byte) []byte {
-	b[0] = ^b[0]
-	return b
-}
-
-func TestIsSameRoute(t *testing.T) {
-	gatewaysWithPrefix := []string{"127.0.0.1/32", "0.0.0.0/0", "123.220.3.14/14", "192.168.10.1/24",
-		"::1/128", "::/128", "2605:143:32:113::1/64", "fe80:4234:242f:1111:5:243:5:4f/88"}
-	nics := []tcpip.NICID{1, 2}
-
-	for _, nic1 := range nics {
-		t.Run(fmt.Sprintf("nic1=%v", nic1), func(t *testing.T) {
-			for _, gp1 := range gatewaysWithPrefix {
-				t.Run(fmt.Sprintf("gp1=%v", gp1), func(t *testing.T) {
-					ip, s, err := net.ParseCIDR(gp1)
-					if err != nil {
-						t.Fatalf("net.ParseCIDR(%v) failed", gp1)
-					}
-					route1 := tcpip.Route{
-						Destination: tcpip.Address(ipToAddress(s.IP)),
-						Mask:        tcpip.AddressMask(s.Mask),
-						Gateway:     tcpip.Address(ipToAddress(ip)),
-						NIC:         nic1,
-					}
-
-					if got := routes.IsSameRoute(route1, route1); got != true {
-						t.Fatalf("got IsSameRoute(%v, %v) = %v, want = %v", route1, route1, got, true)
-					}
-					// Change the NIC
-					route2 := route1
-					route2.NIC = route1.NIC + 1
-					if got := routes.IsSameRoute(route1, route2); got != false {
-						t.Errorf("got IsSameRoute(%v, %v) = %v, want = %v", route1, route2, got, false)
-					}
-					// Change destination.
-					route2 = route1
-					route2.Destination = tcpip.Address(changeByte([]byte(route2.Destination)))
-					if got := routes.IsSameRoute(route1, route2); got != false {
-						t.Errorf("got IsSameRoute(%v, %v) = %v, want = %v", route1, route2, got, false)
-					}
-					// Change gateway.
-					route2 = route1
-					route2.Gateway = tcpip.Address(changeByte([]byte(route2.Gateway)))
-					if got := routes.IsSameRoute(route1, route2); got != false {
-						t.Errorf("got IsSameRoute(%v, %v) = %v, want = %v", route1, route2, got, false)
-					}
-					// Remove gateway if possible
-					if !util.IsAny(route1.Gateway) {
-						route2 = route1
-						route2.Gateway = tcpip.Address("")
-						if got := routes.IsSameRoute(route1, route2); got != false {
-							t.Errorf("got IsSameRoute(%v, %v) = %v, want = %v", route1, route2, got, false)
-						}
-					}
-				})
-			}
-		})
-	}
-}
-
-func isSameRouteTableImpl(rt1, rt2 []routes.ExtendedRoute, checkAttributes bool) bool {
-	if len(rt1) != len(rt2) {
-		return false
-	}
-	for i, r1 := range rt1 {
-		r2 := rt2[i]
-		if !routes.IsSameRoute(r1.Route, r2.Route) {
-			return false
-		}
-		if checkAttributes && (r1.Metric != r2.Metric || r1.MetricTracksInterface != r2.MetricTracksInterface || r1.Dynamic != r2.Dynamic || r1.Enabled != r2.Enabled) {
-			return false
-		}
-	}
-	return true
-}
-
-func isSameRouteTable(rt1, rt2 []routes.ExtendedRoute) bool {
-	return isSameRouteTableImpl(rt1, rt2, true /* checkAttributes */)
-}
-
-func isSameRouteTableSkippingAttributes(rt1, rt2 []routes.ExtendedRoute) bool {
-	return isSameRouteTableImpl(rt1, rt2, false /* checkAttributes */)
-}
-
-func TestAddRoute(t *testing.T) {
-	for _, tc := range []struct {
-		name  string
-		order []int
-	}{
-		// different orders
-		{"Add1", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
-		{"Add2", []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
-		{"Add3", []int{5, 3, 9, 2, 6, 0, 7, 10, 1, 4, 8}},
-		{"Add4", []int{6, 5, 7, 4, 8, 3, 9, 2, 10, 1, 0}},
-
-		// different orders and duplicates
-		{"Add5", []int{0, 0, 1, 2, 3, 4, 2, 5, 6, 7, 1, 8, 9, 10, 0}},
-		{"Add6", []int{10, 9, 8, 4, 7, 6, 5, 2, 4, 3, 2, 1, 0, 5, 5}},
-		{"Add7", []int{5, 3, 9, 2, 6, 0, 7, 10, 10, 1, 3, 4, 8}},
-		{"Add8", []int{6, 6, 6, 5, 7, 4, 8, 3, 9, 9, 2, 7, 10, 1, 0}},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			// Create route table by adding all routes in the given order.
-			tb := routes.RouteTable{}
-			for _, j := range tc.order {
-				r := testRouteTable[j]
-				tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
-			}
-			tableWanted := testRouteTable
-			tableGot := tb.GetExtendedRouteTable()
-			if len(tableGot) != len(tableWanted) {
-				t.Fatalf("got len(table) = %v, want len(table) = %v", len(tableGot), len(tableWanted))
-			}
-			if !isSameRouteTable(tableGot, tableWanted) {
-				t.Errorf("got\n%v, want\n%v", tableGot, tableWanted)
-			}
-		})
-	}
-
-	// Adding a route that already exists but with different dynamic/enabled
-	// attributes will just overwrite the entry with the new attributes. The
-	// position in the table stays the same.
-	t.Run("Changing dynamic/enabled", func(t *testing.T) {
-		tb := routes.RouteTable{}
-		tb.Set(testRouteTable)
-		for i, r := range testRouteTable {
-			r.Dynamic = !r.Dynamic
-			tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
-			tableWanted := testRouteTable
-			tableGot := tb.GetExtendedRouteTable()
-			if tableGot[i].Dynamic != r.Dynamic {
-				t.Errorf("got tableGot[%d].Dynamic = %v, want %v", i, tableGot[i].Dynamic, r.Dynamic)
-			}
-			if !isSameRouteTableSkippingAttributes(tableGot, tableWanted) {
-				t.Errorf("got\n%v, want\n%v", tableGot, tableWanted)
-			}
-
-			r.Enabled = !r.Enabled
-			tb.AddRoute(r.Route, r.Metric, r.MetricTracksInterface, r.Dynamic, r.Enabled)
-			tableGot = tb.GetExtendedRouteTable()
-			if tableGot[i].Enabled != r.Enabled {
-				t.Errorf("got tableGot[%d].Enabled = %v, want %v", i, tableGot[i].Enabled, r.Enabled)
-			}
-			if !isSameRouteTableSkippingAttributes(tableGot, tableWanted) {
-				t.Errorf("got\n%v, want\n%v", tableGot, tableWanted)
-			}
-		}
-	})
-
-	// The metric is used as a tie-breaker when routes have the same prefix length
-	t.Run("Changing metric", func(t *testing.T) {
-		r0 := createRoute(1, "0.0.0.0/0", "192.168.1.1")
-		r1 := createRoute(2, "0.0.0.0/0", "192.168.100.10")
-
-		// 1.test - r0 gets lower metric.
-		tb := routes.RouteTable{}
-		tb.AddRoute(r0, 100, true, true, true)
-		tb.AddRoute(r1, 200, true, true, true)
-		tableGot := tb.GetExtendedRouteTable()
-		if !routes.IsSameRoute(r0, tableGot[0].Route) || !routes.IsSameRoute(r1, tableGot[1].Route) {
-			t.Errorf("got %v, %v, want %v, %v", tableGot[0].Route, tableGot[1].Route, r0, r1)
-		}
-
-		// 2.test - r1 gets lower metric.
-		tb = routes.RouteTable{}
-		tb.AddRoute(r0, 200, true, true, true)
-		tb.AddRoute(r1, 100, true, true, true)
-		tableGot = tb.GetExtendedRouteTable()
-		if !routes.IsSameRoute(r0, tableGot[1].Route) || !routes.IsSameRoute(r1, tableGot[0].Route) {
-			t.Errorf("got %v, %v, want %v, %v", tableGot[0].Route, tableGot[1].Route, r1, r0)
-		}
-	})
-}
-
-func TestDelRoute(t *testing.T) {
-	for _, tc := range []struct {
-		name  string
-		order []int
-	}{
-		{"Del1", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
-		{"Del2", []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
-		{"Del3", []int{6, 5, 7, 4, 8, 3, 9, 2, 10, 1, 0}},
-		{"Del4", []int{0}},
-		{"Del5", []int{1}},
-		{"Del6", []int{9}},
-		{"Del7", []int{0, 0, 1, 1, 5, 5, 9, 9, 10, 10}},
-		{"Del8", []int{5, 8, 5, 0, 1, 9, 1, 5}},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable)
-			validRoutes := make([]bool, len(testRouteTable))
-			for i, _ := range validRoutes {
-				validRoutes[i] = true
-			}
-			for _, d := range tc.order {
-				toDel := testRouteTable[d]
-				tb.DelRoute(toDel.Route)
-				validRoutes[d] = false
-			}
-			tableGot := tb.GetExtendedRouteTable()
-			tableWant := []routes.ExtendedRoute{}
-			for i, valid := range validRoutes {
-				if valid {
-					tableWant = append(tableWant, testRouteTable[i])
-				}
-			}
-			if !isSameRouteTable(tableGot, tableWant) {
-				t.Errorf("got\n%v, want\n%v", tableGot, tableWant)
-			}
-		})
-	}
-}
-
-func TestUpdateMetricByInterface(t *testing.T) {
-	// Updating the metric for the an interface updates the metric for all routes
-	// that track that interface.
-	for nicid := tcpip.NICID(1); nicid <= 4; nicid++ {
-		t.Run(fmt.Sprintf("Change-NIC-%d-metric-updates-route-metric", nicid), func(t *testing.T) {
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable)
-			newMetric := routes.Metric(1000 + nicid)
-			// Verify existing table does not use the new metric yet.
-			tableGot := tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if r.Metric == newMetric {
-					t.Fatalf("Error: r.Metric said to invalid value before test start")
-				}
-			}
-
-			tb.UpdateMetricByInterface(nicid, newMetric)
-
-			tableGot = tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if r.Route.NIC != nicid {
-					continue
-				}
-				if !r.MetricTracksInterface && r.Metric == newMetric {
-					t.Errorf("got MetricTracksInterface==false && Metric=%v, want metric!=%v", r.Metric, newMetric)
-				}
-				if r.MetricTracksInterface && r.Metric != newMetric {
-					t.Errorf("got MetricTracksInterface==true && Metric=%v, want metric=%v", r.Metric, newMetric)
-				}
-			}
-		})
-	}
-
-	// A metric change should trigger a re-sort of the routing table.
-	t.Run(fmt.Sprint("Change-metric-resorts-table"), func(t *testing.T) {
-		r0 := createRoute(0, "0.0.0.0/0", "192.168.1.1")
-		r1 := createRoute(1, "0.0.0.0/0", "192.168.100.10")
-
-		// Initially r0 has lower metric and is ahead in the table.
-		tb := routes.RouteTable{}
-		tb.AddRoute(r0, 100, true, true, true)
-		tb.AddRoute(r1, 200, true, true, true)
-		tableGot := tb.GetExtendedRouteTable()
-		if !routes.IsSameRoute(r0, tableGot[0].Route) || !routes.IsSameRoute(r1, tableGot[1].Route) {
-			t.Errorf("got %v, %v, want %v, %v", tableGot[0].Route, tableGot[1].Route, r0, r1)
-		}
-
-		// Lowering nic1's metric should be reflected in r1's metric and promote it
-		// in the route table.
-		tb.UpdateMetricByInterface(1, 50)
-		tableGot = tb.GetExtendedRouteTable()
-		if !routes.IsSameRoute(r0, tableGot[1].Route) || !routes.IsSameRoute(r1, tableGot[0].Route) {
-			t.Errorf("got %v, %v, want %v, %v", tableGot[0].Route, tableGot[1].Route, r1, r0)
-		}
-	})
-}
-
-func TestUpdateRoutesByInterface(t *testing.T) {
-	for nicid := tcpip.NICID(1); nicid <= 4; nicid++ {
-		// Test the normal case where on DOWN netstack removes dynamic routes and
-		// disables static ones (ActionDeleteDynamicDisableStatic), and on up
-		// it re-enables static routes (ActionEnableStatic).
-		t.Run(fmt.Sprintf("Down-Up_NIC-%d", nicid), func(t *testing.T) {
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable)
-			tableGot := tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if !r.Enabled {
-					t.Errorf("Error: Route %v is disabled before test start", r)
-				}
-			}
-
-			// Down -> Remove dynamic routes and disable static ones.
-			tb.UpdateRoutesByInterface(nicid, routes.ActionDeleteDynamicDisableStatic)
-
-			// Verify all dynamic routes to this NIC are gone, and static ones are
-			// disabled.
-			tableGot = tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if r.Route.NIC != nicid {
-					continue
-				}
-				if r.Enabled {
-					t.Errorf("got %v = enabled, want = disabled", r)
-				}
-				if r.Dynamic {
-					t.Errorf("got dynamic %v, want it removed", r.Metric)
-				}
-			}
-
-			// Up -> Re-enable static routes.
-			tb.UpdateRoutesByInterface(nicid, routes.ActionEnableStatic)
-
-			// Verify dynamic routes to this NIC are still gone, and static ones are
-			// enabled.
-			tableGot = tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if r.Route.NIC != nicid {
-					continue
-				}
-				if !r.Enabled {
-					t.Errorf("got %v = enabled, want = disabled", r)
-				}
-				if r.Dynamic {
-					t.Errorf("got dynamic %v, want it removed", r.Metric)
-				}
-			}
-		})
-
-		// Test the special case where the interface is removed and in response all
-		// routes to this interface are removed (ActionDeleteAll).
-		t.Run(fmt.Sprintf("Remove_NIC-%d", nicid), func(t *testing.T) {
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable)
-			tableGot := tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if !r.Enabled {
-					t.Errorf("Error: Route %v is disabled before test start", r)
-				}
-			}
-
-			// Remove all routes to nicid.
-			tb.UpdateRoutesByInterface(nicid, routes.ActionDeleteAll)
-
-			// Verify all routes to this NIC are gone.
-			tableGot = tb.GetExtendedRouteTable()
-			for _, r := range tableGot {
-				if r.Route.NIC == nicid {
-					t.Errorf("got route %v pointing to NIC-%d, want none", r, nicid)
-				}
-			}
-		})
-	}
-}
-
-func TestGetNetstackTable(t *testing.T) {
-	for _, tc := range []struct {
-		name     string
-		disabled []int
-	}{
-		{"GetNsTable1", []int{}},
-		{"GetNsTable2", []int{0}},
-		{"GetNsTable3", []int{10}},
-		{"GetNsTable4", []int{1}},
-		{"GetNsTable5", []int{9}},
-		{"GetNsTable6", []int{3, 5, 8}},
-		{"GetNsTable7", []int{0, 1, 5, 6, 8, 9, 10}},
-		{"GetNsTable8", []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			// We have no way to directly disable routes in the route table, but we
-			// can use the Set() command to set a table with pre-disabled routes.
-			testRouteTable2 := make([]routes.ExtendedRoute, len(testRouteTable))
-			copy(testRouteTable2, testRouteTable)
-			// Disable a few routes.
-			for _, i := range tc.disabled {
-				testRouteTable2[i].Enabled = false
-			}
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable2)
-
-			tableGot := tb.GetNetstackTable()
-
-			// Verify no disabled routes are in the Netstack table we got.
-			i := 0
-			for _, r := range testRouteTable2 {
-				if r.Enabled {
-					if !routes.IsSameRoute(tableGot[i], r.Route) {
-						t.Errorf("got = %v, want = %v", tableGot[i], r.Route)
-					}
-					i += 1
-				}
-			}
-		})
-	}
-}
-
-func TestFindNIC(t *testing.T) {
-	for _, tc := range []struct {
-		name      string
-		addr      string
-		nicWanted tcpip.NICID // 0 means not found
-	}{
-		{"FindNic1", "127.0.0.1", 1},
-		{"FindNic2", "127.0.0.0", 0},
-		{"FindNic3", "192.168.1.234", 4},
-		{"FindNic4", "192.168.1.1", 4},
-		{"FindNic5", "192.168.2.1", 0},
-		{"FindNic6", "192.168.100.1", 2},
-		{"FindNic7", "192.168.100.10", 2},
-		{"FindNic8", "192.168.101.10", 0},
-		{"FindNic9", "10.1.2.1", 3},
-		{"FindNic10", "10.1.3.1", 3},
-		{"FindNic11", "10.1.4.1", 0},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			tb := routes.RouteTable{}
-			tb.Set(testRouteTable)
-
-			nicGot, err := tb.FindNIC(ipStringToAddress(tc.addr))
-			if err != nil && tc.nicWanted > 0 {
-				t.Errorf("got nic = <unknown>, want = %v", tc.nicWanted)
-			} else if err == nil && tc.nicWanted != nicGot {
-				t.Errorf("got nic = %d, want = %v", nicGot, tc.nicWanted)
-			}
-		})
-	}
-}
diff --git a/garnet/go/src/netstack/util/parse.go b/garnet/go/src/netstack/util/parse.go
index 68a6222..b46695d 100644
--- a/garnet/go/src/netstack/util/parse.go
+++ b/garnet/go/src/netstack/util/parse.go
@@ -11,19 +11,6 @@
 	"github.com/google/netstack/tcpip"
 )
 
-func IsAny(a tcpip.Address) bool {
-	// An empty address is not the same as ANY.
-	if len(a) == 0 {
-		return false
-	}
-	for _, n := range a {
-		if n != 0 {
-			return false
-		}
-	}
-	return true
-}
-
 func ApplyMask(addr tcpip.Address, mask tcpip.AddressMask) tcpip.Address {
 	return tcpip.Address(net.IP(addr).Mask(net.IPMask(mask)))
 }
@@ -32,11 +19,6 @@
 	return tcpip.AddressMask(net.CIDRMask(ones, bits))
 }
 
-func PrefixLength(mask tcpip.AddressMask) int {
-	bits, _ := net.IPMask(mask).Size()
-	return bits
-}
-
 func ipToAddress(ip net.IP) tcpip.Address {
 	if v4 := ip.To4(); v4 != nil {
 		return tcpip.Address(v4)
diff --git a/garnet/go/src/netstack/util/parse_test.go b/garnet/go/src/netstack/util/parse_test.go
index 6923cdcd..b801ad6 100644
--- a/garnet/go/src/netstack/util/parse_test.go
+++ b/garnet/go/src/netstack/util/parse_test.go
@@ -68,70 +68,3 @@
 		}
 	}
 }
-
-func TestIsAny(t *testing.T) {
-	for _, tc := range []struct {
-		name string
-		addr tcpip.Address
-		want bool
-	}{
-		{"IPv4-Empty", "", false},
-		{"IPv4-Zero", "\x00\x00\x00\x00", true},
-		{"IPv4-Loopback", "\x7f\x00\x00\x01", false},
-		{"IPv4-Broadcast", "\xff\xff\xff\xff", false},
-		{"IPv4-Regular1", "\x00\x00\x00\x01", false},
-		{"IPv4-Regular2", "\x00\x00\x01\x00", false},
-		{"IPv4-Regular3", "\x00\x01\x00\x00", false},
-		{"IPv4-Regular4", "\x01\x00\x00\x00", false},
-		{"IPv4-Regular5", "\x01\x01\x01\x01", false},
-		{"IPv4-Regular6", "\x11\x22\x33\x44", false},
-		{"IPv6-Empty", "", false},
-		{"IPv6-Zero", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", true},
-		{"IPv6-Loopback", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", false},
-		{"IPv6-Broadcast", "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", false},
-		{"IPv6-Regular1", "\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", false},
-		{"IPv6-Regular2", "\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00", false},
-		{"IPv6-Regular3", "\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00", false},
-		{"IPv6-Regular4", "\x00\xaa\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", false},
-		{"IPv6-Regular5", "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01", false},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			if got := util.IsAny(tc.addr); got != tc.want {
-				t.Fatalf("IsAny(%v) = %v, want = %v", tc.addr, got, tc.want)
-			}
-		})
-	}
-}
-
-func TestPrefixLength(t *testing.T) {
-	for _, tc := range []struct {
-		name string
-		mask tcpip.AddressMask
-		want int
-	}{
-		{"IPv4-Empty", "", 0},
-		{"IPv4-0", "\x00\x00\x00\x00", 0},
-		{"IPv4-3", "\xe0\x00\x00\x00", 3},
-		{"IPv4-7", "\xfe\x00\x00\x00", 7},
-		{"IPv4-8", "\xff\x00\x00\x00", 8},
-		{"IPv4-12", "\xff\xf0\x00\x00", 12},
-		{"IPv4-16", "\xff\xff\x00\x00", 16},
-		{"IPv4-24", "\xff\xff\xff\x00", 24},
-		{"IPv4-29", "\xff\xff\xff\xfc", 30},
-		{"IPv4-32", "\xff\xff\xff\xff", 32},
-		{"IPv6-Empty", "", 0},
-		{"IPv6-0", "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0},
-		{"IPv6-5", "\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 5},
-		{"IPv6-8", "\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 8},
-		{"IPv6-22", "\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 22},
-		{"IPv6-73", "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00", 73},
-		{"IPv6-123", "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0", 123},
-		{"IPv6-128", "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", 128},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			if got := util.PrefixLength(tc.mask); got != tc.want {
-				t.Fatalf("PrefixLength(%v) = %v, want = %v", tc.mask, got, tc.want)
-			}
-		})
-	}
-}
diff --git a/garnet/packages/tests/netstack b/garnet/packages/tests/netstack
index 39fd20b..3540a69 100644
--- a/garnet/packages/tests/netstack
+++ b/garnet/packages/tests/netstack
@@ -13,7 +13,6 @@
     "host_tests": [
         "//garnet/go/src/netstack/dns:netstack_dns_test",
         "//garnet/go/src/netstack/link/bridge:netstack_link_bridge_test",
-        "//garnet/go/src/netstack/routes:netstack_routes_test",
         "//garnet/go/src/netstack/util:netstack_util_test"
     ]
 }
diff --git a/sdk/fidl/fuchsia.netstack/netstack.fidl b/sdk/fidl/fuchsia.netstack/netstack.fidl
index de29872..de994a2 100644
--- a/sdk/fidl/fuchsia.netstack/netstack.fidl
+++ b/sdk/fidl/fuchsia.netstack/netstack.fidl
@@ -111,23 +111,6 @@
     vector<uint8> hwaddr;
 };
 
-// New version that includes a metric value.
-// TODO(NET-2078): Move this to NetInterface once Chromium stops using
-// netstack.fidl.
-struct NetInterface2 {
-    uint32 id;
-    uint32 flags;
-    uint32 features;
-    uint32 configuration;
-    uint32 metric;
-    string name;
-    fuchsia.net.IpAddress addr;
-    fuchsia.net.IpAddress netmask;
-    fuchsia.net.IpAddress broadaddr;
-    vector<fuchsia.net.Subnet> ipv6addrs;
-    vector<uint8> hwaddr;
-};
-
 // Flags for NetInterface.flags.
 const uint32 NetInterfaceFlagUp = 0x01; // Set if the interface is up.
 const uint32 NetInterfaceFlagDhcp = 0x02; // Set if DHCP is enabled.
@@ -139,17 +122,6 @@
     uint32 nicid;
 };
 
-// New version that includes a metric value.
-// TODO(NET-2078): Move this to NetInterface once Chromium stops using
-// netstack.fidl.
-struct RouteTableEntry2 {
-    fuchsia.net.IpAddress destination;
-    fuchsia.net.IpAddress netmask;
-    fuchsia.net.IpAddress? gateway;
-    uint32 nicid;
-    uint32 metric;
-};
-
 struct SocketAddress {
     fuchsia.net.IpAddress addr;
     uint16 port;
@@ -169,15 +141,13 @@
 
     // Returns the list of registered network interfaces.
     GetInterfaces() -> (vector<NetInterface> interfaces);
-    GetInterfaces2() -> (vector<NetInterface2> interfaces);
 
     // DEPRECATED: see devicesettings.fidl
     // Returns the netstack's node name.
     // 5: GetNodeName() -> (string node_name);
 
-    // Returns current route table.
+    // Don't use this for read-modify-write.  Use StartRouteTableTransaction instead.
     GetRouteTable() -> (vector<RouteTableEntry> rt);
-    GetRouteTable2() -> (vector<RouteTableEntry2> rt);
 
     // TODO (porce): Separate interfaces.
     GetStats(uint32 nicid) -> (NetInterfaceStats stats);
@@ -188,6 +158,9 @@
     // Sets the status (up or down) for the interface with the given nicid.
     SetInterfaceStatus(uint32 nicid, bool enabled);
 
+    // DEPRECATED: Use StartRouteTableTransaction and SetRouteTable from there.
+    // 9: SetRouteTable(vector<RouteTableEntry> rt);
+
     // Sets the address for the interface with the given nicid.
     // Masks off addr.PrefixLen bits from addr.Addr to set the subnet.
     SetInterfaceAddress(uint32 nicid, fuchsia.net.IpAddress addr, uint8 prefixLen) -> (NetErr result);
@@ -196,9 +169,6 @@
     // Masks off addr.PrefixLen bits from addr.Addr to set the subnet.
     RemoveInterfaceAddress(uint32 nicid, fuchsia.net.IpAddress addr, uint8 prefixLen) -> (NetErr result);
 
-    // Sets the route metric for the interface with the given nicid.
-    SetInterfaceMetric(uint32 nicid, uint32 metric) -> (NetErr result);
-
     SetDhcpClientStatus(uint32 nicid, bool enabled) -> (NetErr result);
 
     BridgeInterfaces(vector<uint32> nicids) -> (NetErr result);
@@ -213,11 +183,15 @@
     StartRouteTableTransaction(request<RouteTableTransaction> routeTableTransaction) -> (zx.status status);
 
     -> OnInterfacesChanged(vector<NetInterface> interfaces);
-    -> OnInterfacesChanged2(vector<NetInterface2> interfaces);
 };
 
+// When Commit is called, the most recent SetRouteTable will be
+// committed to the route tables.  Commit may be called multiple times.
 [Discoverable]
 interface RouteTableTransaction {
-    AddRoute(RouteTableEntry2 r) -> (zx.status status);
-    DelRoute(RouteTableEntry2 r) -> (zx.status status);
+    GetRouteTable() -> (vector<RouteTableEntry> rt);
+
+    SetRouteTable(vector<RouteTableEntry> rt);
+
+    Commit() -> (zx.status status);
 };