Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/apps/sdnip/pom.xml b/apps/sdnip/pom.xml
index b1465c1..5eb16f6 100644
--- a/apps/sdnip/pom.xml
+++ b/apps/sdnip/pom.xml
@@ -45,6 +45,12 @@
 
     <dependency>
       <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
       <artifactId>onos-cli</artifactId>
       <version>${project.version}</version>
     </dependency>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
index 400dba0..4056cbe 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.sdnip;
 
+import java.util.List;
+
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
@@ -8,6 +10,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.PointToPointIntent;
+import org.onlab.onos.sdnip.bgp.BgpConstants;
 import org.onlab.onos.sdnip.config.BgpPeer;
 import org.onlab.onos.sdnip.config.BgpSpeaker;
 import org.onlab.onos.sdnip.config.Interface;
@@ -20,8 +23,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-
 /**
  * Manages the connectivity requirements between peers.
  */
@@ -30,37 +31,44 @@
     private static final Logger log = LoggerFactory.getLogger(
             PeerConnectivityManager.class);
 
-    // TODO these shouldn't be defined here
-    private static final short BGP_PORT = 179;
-    private static final int IPV4_BIT_LENGTH = 32;
-
-    private final SdnIpConfigService configInfoService;
+    private final SdnIpConfigService configService;
     private final InterfaceService interfaceService;
     private final IntentService intentService;
 
     private final ApplicationId appId;
 
+    /**
+     * Creates a new PeerConnectivityManager.
+     *
+     * @param appId             the application ID
+     * @param configService     the SDN-IP config service
+     * @param interfaceService  the interface service
+     * @param intentService     the intent service
+     */
     public PeerConnectivityManager(ApplicationId appId,
-                                   SdnIpConfigService configInfoService,
+                                   SdnIpConfigService configService,
                                    InterfaceService interfaceService,
                                    IntentService intentService) {
         this.appId = appId;
-        this.configInfoService = configInfoService;
+        this.configService = configService;
         this.interfaceService = interfaceService;
         this.intentService = intentService;
     }
 
+    /**
+     * Starts the peer connectivity manager.
+     */
     public void start() {
         // TODO are any of these errors?
         if (interfaceService.getInterfaces().isEmpty()) {
 
             log.warn("The interface in configuration file is empty. "
                              + "Thus, the SDN-IP application can not be started.");
-        } else if (configInfoService.getBgpPeers().isEmpty()) {
+        } else if (configService.getBgpPeers().isEmpty()) {
 
             log.warn("The BGP peer in configuration file is empty."
                              + "Thus, the SDN-IP application can not be started.");
-        } else if (configInfoService.getBgpSpeakers() == null) {
+        } else if (configService.getBgpSpeakers() == null) {
 
             log.error("The BGP speaker in configuration file is empty. "
                               + "Thus, the SDN-IP application can not be started.");
@@ -79,7 +87,7 @@
      * for paths from all peers to each BGP speaker.
      */
     private void setupBgpPaths() {
-        for (BgpSpeaker bgpSpeaker : configInfoService.getBgpSpeakers()
+        for (BgpSpeaker bgpSpeaker : configService.getBgpSpeakers()
                 .values()) {
             log.debug("Start to set up BGP paths for BGP speaker: {}",
                       bgpSpeaker);
@@ -88,7 +96,7 @@
             List<InterfaceAddress> interfaceAddresses =
                     bgpSpeaker.interfaceAddresses();
 
-            for (BgpPeer bgpPeer : configInfoService.getBgpPeers().values()) {
+            for (BgpPeer bgpPeer : configService.getBgpPeers().values()) {
 
                 log.debug("Start to set up BGP paths between BGP speaker: {} "
                                   + "to BGP peer: {}", bgpSpeaker, bgpPeer);
@@ -121,16 +129,14 @@
 
                 // install intent for BGP path from BGPd to BGP peer matching
                 // destination TCP port 179
-
-                // TODO: The usage of PacketMatchBuilder will be improved, then we
-                // only need to new the PacketMatchBuilder once.
-                // By then, the code here will be improved accordingly.
                 TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpDst(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpDst((short) BgpConstants.BGP_PORT)
                         .build();
 
                 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -149,9 +155,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpSrc(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpSrc((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent intentMatchSrcTcpPort =
@@ -167,9 +175,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpDst(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpDst((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent reversedIntentMatchDstTcpPort =
@@ -185,9 +195,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpSrc(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpSrc((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent reversedIntentMatchSrcTcpPort =
@@ -211,7 +223,7 @@
      * for paths from all peers to each BGP speaker.
      */
     private void setupIcmpPaths() {
-        for (BgpSpeaker bgpSpeaker : configInfoService.getBgpSpeakers()
+        for (BgpSpeaker bgpSpeaker : configService.getBgpSpeakers()
                 .values()) {
             log.debug("Start to set up ICMP paths for BGP speaker: {}",
                       bgpSpeaker);
@@ -219,7 +231,7 @@
             List<InterfaceAddress> interfaceAddresses = bgpSpeaker
                     .interfaceAddresses();
 
-            for (BgpPeer bgpPeer : configInfoService.getBgpPeers().values()) {
+            for (BgpPeer bgpPeer : configService.getBgpPeers().values()) {
 
                 Interface peerInterface = interfaceService.getInterface(
                         bgpPeer.connectPoint());
@@ -253,8 +265,10 @@
                 TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
                         .build();
 
                 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -271,8 +285,10 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
                         .build();
 
                 PointToPointIntent reversedIntent =
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
index ed5b8df..1d8ef16 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -55,8 +55,6 @@
 /**
  * This class processes BGP route update, translates each update into a intent
  * and submits the intent.
- * <p/>
- * TODO: Make it thread-safe.
  */
 public class Router implements RouteListener {
 
@@ -69,14 +67,13 @@
     // Stores all incoming route updates in a queue.
     private BlockingQueue<RouteUpdate> routeUpdates;
 
-    // The Ip4Address is the next hop address of each route update.
+    // The IpAddress is the next hop address of each route update.
     private SetMultimap<IpAddress, RouteEntry> routesWaitingOnArp;
     private ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> pushedRouteIntents;
 
     private IntentService intentService;
-    //private IProxyArpService proxyArp;
     private HostService hostService;
-    private SdnIpConfigService configInfoService;
+    private SdnIpConfigService configService;
     private InterfaceService interfaceService;
 
     private ExecutorService bgpUpdatesExecutor;
@@ -98,18 +95,19 @@
     /**
      * Class constructor.
      *
+     * @param appId             the application ID
      * @param intentService     the intent service
      * @param hostService       the host service
-     * @param configInfoService the configuration service
+     * @param configService     the configuration service
      * @param interfaceService  the interface service
      */
     public Router(ApplicationId appId, IntentService intentService,
-                  HostService hostService, SdnIpConfigService configInfoService,
+                  HostService hostService, SdnIpConfigService configService,
                   InterfaceService interfaceService) {
         this.appId = appId;
         this.intentService = intentService;
         this.hostService = hostService;
-        this.configInfoService = configInfoService;
+        this.configService = configService;
         this.interfaceService = interfaceService;
 
         bgpRoutes = new ConcurrentInvertedRadixTree<>(
@@ -172,7 +170,7 @@
 
     @Override
     public void update(RouteUpdate routeUpdate) {
-        log.debug("Received new route Update: {}", routeUpdate);
+        log.debug("Received new route update: {}", routeUpdate);
 
         try {
             routeUpdates.put(routeUpdate);
@@ -498,9 +496,11 @@
     private void executeRouteAdd(RouteEntry routeEntry) {
         log.debug("Executing route add: {}", routeEntry);
 
+        // Monitor the IP address so we'll get notified of updates to the MAC
+        // address.
+        hostService.startMonitoringIp(routeEntry.nextHop());
+
         // See if we know the MAC address of the next hop
-        //MacAddress nextHopMacAddress =
-        //proxyArp.getMacAddress(routeEntry.getNextHop());
         MacAddress nextHopMacAddress = null;
         Set<Host> hosts = hostService.getHostsByIp(
                 routeEntry.nextHop().toPrefix());
@@ -511,9 +511,6 @@
 
         if (nextHopMacAddress == null) {
             routesWaitingOnArp.put(routeEntry.nextHop(), routeEntry);
-            //proxyArp.sendArpRequest(routeEntry.getNextHop(), this, true);
-            // TODO maybe just do this for every prefix anyway
-            hostService.startMonitoringIp(routeEntry.nextHop());
             return;
         }
 
@@ -536,11 +533,11 @@
 
         // Find the attachment point (egress interface) of the next hop
         Interface egressInterface;
-        if (configInfoService.getBgpPeers().containsKey(nextHopIpAddress)) {
+        if (configService.getBgpPeers().containsKey(nextHopIpAddress)) {
             // Route to a peer
             log.debug("Route to peer {}", nextHopIpAddress);
             BgpPeer peer =
-                    configInfoService.getBgpPeers().get(nextHopIpAddress);
+                    configService.getBgpPeers().get(nextHopIpAddress);
             egressInterface =
                     interfaceService.getInterface(peer.connectPoint());
         } else {
@@ -593,17 +590,12 @@
         }
 
         // Match the destination IP prefix at the first hop
-        //PacketMatchBuilder builder = new PacketMatchBuilder();
-        //builder.setEtherType(Ethernet.TYPE_IPV4).setDstIpNet(prefix);
-        //PacketMatch packetMatch = builder.build();
         TrafficSelector selector = DefaultTrafficSelector.builder()
                 .matchEthType(Ethernet.TYPE_IPV4)
                 .matchIPDst(prefix)
                 .build();
 
         // Rewrite the destination MAC address
-        //ModifyDstMacAction modifyDstMacAction =
-        //new ModifyDstMacAction(nextHopMacAddress);
         TrafficTreatment treatment = DefaultTrafficTreatment.builder()
                 .setEthDst(nextHopMacAddress)
                 .build();
@@ -635,10 +627,6 @@
             log.debug("Processing route delete: {}", routeEntry);
             IpPrefix prefix = routeEntry.prefix();
 
-            // TODO check the change of logic here - remove doesn't check that
-            // the route entry was what we expected (and we can't do this
-            // concurrently)
-
             if (bgpRoutes.remove(RouteEntry.createBinaryString(prefix))) {
                 //
                 // Only delete flows if an entry was actually removed from the
@@ -680,17 +668,19 @@
     }
 
     /**
-     * This method handles the prefixes which are waiting for ARP replies for
-     * MAC addresses of next hops.
+     * Signals the Router that the MAC to IP mapping has potentially been
+     * updated. This has the effect of updating the MAC address for any
+     * installed prefixes if it has changed, as well as installing any pending
+     * prefixes that were waiting for MAC resolution.
      *
-     * @param ipAddress  next hop router IP address, for which we sent ARP
-     *                   request out
-     * @param macAddress MAC address which is relative to the ipAddress
+     * @param ipAddress the IP address that an event was received for
+     * @param macAddress the most recently known MAC address for the IP address
      */
-    //@Override
-    // TODO change name
-    public void arpResponse(IpAddress ipAddress, MacAddress macAddress) {
-        log.debug("Received ARP response: {} => {}", ipAddress, macAddress);
+    private void updateMac(IpAddress ipAddress, MacAddress macAddress) {
+        log.debug("Received updated MAC info: {} => {}", ipAddress, macAddress);
+
+        // TODO here we should check whether the next hop for any of our
+        // installed prefixes has changed, not just prefixes pending installation.
 
         // We synchronize on this to prevent changes to the radix tree
         // while we're pushing intents. If the tree changes, the
@@ -708,8 +698,6 @@
                         bgpRoutes.getValueForExactKey(binaryString);
                 if (foundRouteEntry != null &&
                         foundRouteEntry.nextHop().equals(routeEntry.nextHop())) {
-                    log.debug("Pushing prefix {} next hop {}",
-                              routeEntry.prefix(), routeEntry.nextHop());
                     // We only push prefix flows if the prefix is still in the
                     // radix tree and the next hop is the same as our
                     // update.
@@ -717,9 +705,8 @@
                     // for the ARP, or the next hop could have changed.
                     addRouteIntentToNextHop(prefix, ipAddress, macAddress);
                 } else {
-                    log.debug("Received ARP response, but {}/{} is no longer in"
-                                      + " the radix tree", routeEntry.prefix(),
-                              routeEntry.nextHop());
+                    log.debug("{} has been revoked before the MAC was resolved",
+                            routeEntry);
                 }
             }
         }
@@ -769,7 +756,7 @@
                     event.type() == HostEvent.Type.HOST_UPDATED) {
                 Host host = event.subject();
                 for (IpPrefix ip : host.ipAddresses()) {
-                    arpResponse(ip.toIpAddress(), host.mac());
+                    updateMac(ip.toIpAddress(), host.mac());
                 }
             }
         }
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index d66ec9c..5ce37c8 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -26,7 +26,7 @@
 @Service
 public class SdnIp implements SdnIpService {
 
-    private static final String SDN_ID_APP = "org.onlab.onos.sdnip";
+    private static final String SDN_IP_APP = "org.onlab.onos.sdnip";
 
     private final Logger log = getLogger(getClass());
 
@@ -53,8 +53,10 @@
 
         InterfaceService interfaceService = new HostToInterfaceAdaptor(hostService);
 
-        ApplicationId appId = coreService.registerApplication(SDN_ID_APP);
-        peerConnectivity = new PeerConnectivityManager(appId, config, interfaceService, intentService);
+        ApplicationId appId = coreService.registerApplication(SDN_IP_APP);
+
+        peerConnectivity = new PeerConnectivityManager(appId, config,
+                interfaceService, intentService);
         peerConnectivity.start();
 
         router = new Router(appId, intentService, hostService, config, interfaceService);
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
index 9ab5916..c61e1ec 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
@@ -1,7 +1,9 @@
 package org.onlab.onos.sdnip;
 
+import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.reset;
 import static org.easymock.EasyMock.verify;
@@ -15,6 +17,8 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.junit.TestUtils.TestUtilsException;
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultHost;
@@ -27,6 +31,7 @@
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.host.HostListener;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
@@ -39,8 +44,6 @@
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
-import org.onlab.util.TestUtils;
-import org.onlab.util.TestUtils.TestUtilsException;
 
 import com.google.common.collect.Sets;
 
@@ -55,10 +58,17 @@
     private IntentService intentService;
     private HostService hostService;
 
-    private Map<IpAddress, BgpPeer> bgpPeers;
-    private Map<IpAddress, BgpPeer> configuredPeers;
-    private Set<Interface> interfaces;
-    private Set<Interface> configuredInterfaces;
+    private static final ConnectPoint SW1_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000001"),
+            PortNumber.portNumber(1));
+
+    private static final ConnectPoint SW2_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000002"),
+            PortNumber.portNumber(1));
+
+    private static final ConnectPoint SW3_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000003"),
+            PortNumber.portNumber(1));
 
     private static final ApplicationId APPID = new ApplicationId() {
         @Override
@@ -76,55 +86,12 @@
 
     @Before
     public void setUp() throws Exception {
-        bgpPeers = setUpBgpPeers();
-        interfaces = setUpInterfaces();
-        initRouter();
-    }
+        setUpBgpPeers();
 
-    /**
-     * Initializes Router class.
-     */
-    private void initRouter() {
+        setUpInterfaceService();
+        setUpHostService();
 
         intentService = createMock(IntentService.class);
-        hostService = createMock(HostService.class);
-
-        interfaceService = createMock(InterfaceService.class);
-        expect(interfaceService.getInterfaces()).andReturn(
-                interfaces).anyTimes();
-
-        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
-        Interface expectedInterface =
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber("1")),
-                        ipAddressesOnSw1Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:01"));
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber(1));
-        expect(interfaceService.getInterface(egressPoint)).andReturn(
-                expectedInterface).anyTimes();
-
-        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
-        Interface expectedInterfaceNew =
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber("1")),
-                        ipAddressesOnSw2Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:02"));
-        ConnectPoint egressPointNew = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber(1));
-        expect(interfaceService.getInterface(egressPointNew)).andReturn(
-                expectedInterfaceNew).anyTimes();
-        replay(interfaceService);
-
-        sdnIpConfigService = createMock(SdnIpConfigService.class);
-        expect(sdnIpConfigService.getBgpPeers()).andReturn(bgpPeers).anyTimes();
-        replay(sdnIpConfigService);
 
         router = new Router(APPID, intentService,
                 hostService, sdnIpConfigService, interfaceService);
@@ -132,67 +99,99 @@
 
     /**
      * Sets up BGP peers in external networks.
-     *
-     * @return configured BGP peers as a Map from peer IP address to BgpPeer
      */
-    private Map<IpAddress, BgpPeer> setUpBgpPeers() {
+    private void setUpBgpPeers() {
 
-        configuredPeers = new HashMap<>();
+        Map<IpAddress, BgpPeer> peers = new HashMap<>();
 
         String peerSw1Eth1 = "192.168.10.1";
-        configuredPeers.put(IpAddress.valueOf(peerSw1Eth1),
+        peers.put(IpAddress.valueOf(peerSw1Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:01", 1, peerSw1Eth1));
 
         // Two BGP peers are connected to switch 2 port 1.
         String peer1Sw2Eth1 = "192.168.20.1";
-        configuredPeers.put(IpAddress.valueOf(peer1Sw2Eth1),
+        peers.put(IpAddress.valueOf(peer1Sw2Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:02", 1, peer1Sw2Eth1));
 
         String peer2Sw2Eth1 = "192.168.20.2";
-        configuredPeers.put(IpAddress.valueOf(peer2Sw2Eth1),
+        peers.put(IpAddress.valueOf(peer2Sw2Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:02", 1, peer2Sw2Eth1));
 
-        return configuredPeers;
+        sdnIpConfigService = createMock(SdnIpConfigService.class);
+        expect(sdnIpConfigService.getBgpPeers()).andReturn(peers).anyTimes();
+        replay(sdnIpConfigService);
+
     }
 
     /**
      * Sets up logical interfaces, which emulate the configured interfaces
      * in SDN-IP application.
-     *
-     * @return configured interfaces as a Set
      */
-    private Set<Interface> setUpInterfaces() {
+    private void setUpInterfaceService() {
+        interfaceService = createMock(InterfaceService.class);
 
-        configuredInterfaces = Sets.newHashSet();
+        Set<Interface> interfaces = Sets.newHashSet();
 
-        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw1Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:01")));
+        Interface sw1Eth1 = new Interface(SW1_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.10.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:01"));
 
-        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw2Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:02")));
+        expect(interfaceService.getInterface(SW1_ETH1)).andReturn(sw1Eth1).anyTimes();
+        interfaces.add(sw1Eth1);
 
-        Set<IpPrefix> ipAddressesOnSw3Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw3Eth1.add(IpPrefix.valueOf("192.168.30.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000003"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw3Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:03")));
+        Interface sw2Eth1 = new Interface(SW2_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.20.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:02"));
 
-        return configuredInterfaces;
+        expect(interfaceService.getInterface(SW2_ETH1)).andReturn(sw2Eth1).anyTimes();
+        interfaces.add(sw2Eth1);
+
+        Interface sw3Eth1 = new Interface(SW3_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.30.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:03"));
+
+        expect(interfaceService.getInterface(SW3_ETH1)).andReturn(sw3Eth1).anyTimes();
+        interfaces.add(sw3Eth1);
+
+        expect(interfaceService.getInterfaces()).andReturn(interfaces).anyTimes();
+
+        replay(interfaceService);
+    }
+
+    /**
+     * Sets up the host service with details of some hosts.
+     */
+    private void setUpHostService() {
+        hostService = createMock(HostService.class);
+
+        hostService.addListener(anyObject(HostListener.class));
+        expectLastCall().anyTimes();
+
+        IpPrefix host1Address = IpPrefix.valueOf("192.168.10.1/32");
+        Host host1 = new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:01"), VlanId.NONE,
+                new HostLocation(SW1_ETH1, 1),
+                        Sets.newHashSet(host1Address));
+
+        expect(hostService.getHostsByIp(host1Address))
+                .andReturn(Sets.newHashSet(host1)).anyTimes();
+        hostService.startMonitoringIp(host1Address.toIpAddress());
+        expectLastCall().anyTimes();
+
+
+        IpPrefix host2Address = IpPrefix.valueOf("192.168.20.1/32");
+        Host host2 = new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:02"), VlanId.NONE,
+                new HostLocation(SW2_ETH1, 1),
+                        Sets.newHashSet(host2Address));
+
+        expect(hostService.getHostsByIp(host2Address))
+                .andReturn(Sets.newHashSet(host2)).anyTimes();
+        hostService.startMonitoringIp(host2Address.toIpAddress());
+        expectLastCall().anyTimes();
+
+
+        replay(hostService);
     }
 
     /**
@@ -200,7 +199,6 @@
      */
     @Test
     public void testProcessRouteAdd() throws TestUtilsException {
-
         // Construct a route entry
         RouteEntry routeEntry = new RouteEntry(
                 IpPrefix.valueOf("1.1.1.0/24"),
@@ -217,36 +215,13 @@
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
-
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
-
-        // Reset host service
-        reset(hostService);
-        Set<Host> hosts = new HashSet<Host>(1);
-        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
-        ipPrefixes.add(IpPrefix.valueOf("192.168.10.1/32"));
-        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
-                MacAddress.valueOf("00:00:00:00:00:01"), VlanId.NONE,
-                new HostLocation(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber(1), 1),
-                        ipPrefixes));
-        expect(hostService.getHostsByIp(
-                IpPrefix.valueOf("192.168.10.1/32"))).andReturn(hosts);
-        replay(hostService);
+                        ingressPoints, SW1_ETH1);
 
         // Set up test expectation
         reset(intentService);
@@ -274,7 +249,6 @@
      */
     @Test
     public void testRouteUpdate() throws TestUtilsException {
-
         // Firstly add a route
         testProcessRouteAdd();
 
@@ -293,22 +267,14 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
-
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
+                        ingressPoints, SW1_ETH1);
 
         // Start to construct a new route entry and new intent
         RouteEntry routeEntryUpdate = new RouteEntry(
@@ -325,38 +291,16 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilderNew.setEthDst(MacAddress.valueOf("00:00:00:00:00:02"));
 
-        ConnectPoint egressPointNew = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1"));
 
         Set<ConnectPoint> ingressPointsNew = new HashSet<ConnectPoint>();
-        ingressPointsNew.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1")));
-        ingressPointsNew.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPointsNew.add(SW1_ETH1);
+        ingressPointsNew.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intentNew =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilderNew.build(),
                         treatmentBuilderNew.build(),
-                        ingressPointsNew, egressPointNew);
-
-        // Reset host service
-        reset(hostService);
-        Set<Host> hosts = new HashSet<Host>(1);
-        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
-        ipPrefixes.add(IpPrefix.valueOf("192.168.20.1/32"));
-        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
-                MacAddress.valueOf("00:00:00:00:00:02"), VlanId.NONE,
-                new HostLocation(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber(1), 1),
-                        ipPrefixes));
-        expect(hostService.getHostsByIp(
-                IpPrefix.valueOf("192.168.20.1/32"))).andReturn(hosts);
-        replay(hostService);
+                        ingressPointsNew, SW2_ETH1);
 
         // Set up test expectation
         reset(intentService);
@@ -383,7 +327,6 @@
      */
     @Test
     public void testProcessRouteDelete() throws TestUtilsException {
-
         // Firstly add a route
         testProcessRouteAdd();
 
@@ -402,22 +345,14 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
-
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
+                        ingressPoints, SW1_ETH1);
 
         // Set up expectation
         reset(intentService);
@@ -442,7 +377,6 @@
      */
     @Test
     public void testLocalRouteAdd() throws TestUtilsException {
-
         // Construct a route entry, the next hop is the local BGP speaker
         RouteEntry routeEntry = new RouteEntry(
                 IpPrefix.valueOf("1.1.1.0/24"), IpAddress.valueOf("0.0.0.0"));
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
index 99e3be1..f9dbb7b 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
@@ -26,12 +26,12 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.junit.TestUtils.TestUtilsException;
 import org.onlab.onos.sdnip.RouteListener;
 import org.onlab.onos.sdnip.RouteUpdate;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
-import org.onlab.util.TestUtils;
-import org.onlab.util.TestUtils.TestUtilsException;
 
 import com.google.common.net.InetAddresses;
 
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
index 471e397..3e21187 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
@@ -6,6 +6,7 @@
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
  * This class tests the immutability, equality, and non-equality of
@@ -17,7 +18,7 @@
      */
     @Test
     public void intentIdFollowsGuidelineForImmutableObject() {
-        ImmutableClassChecker.assertThatClassIsImmutable(IntentId.class);
+        assertThatClassIsImmutable(IntentId.class);
     }
 
     /**
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
index 4ebee73..2b56e88 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
@@ -11,6 +11,7 @@
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.hid;
 
 /**
@@ -104,6 +105,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.assertThatClassIsImmutable(HostToHostIntent.class);
+        assertThatClassIsImmutable(HostToHostIntent.class);
     }
 }
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
index 65aa71f..42b5b28 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
@@ -4,6 +4,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.link;
 
 import java.util.HashSet;
@@ -154,6 +155,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.assertThatClassIsImmutable(LinkCollectionIntent.class);
+        assertThatClassIsImmutable(LinkCollectionIntent.class);
     }
 }
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
index 09dfcb2..0e4f706 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
@@ -15,6 +15,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.connectPoint;
 
 /**
@@ -135,7 +136,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.
-                assertThatClassIsImmutable(MultiPointToSinglePointIntent.class);
+        assertThatClassIsImmutable(MultiPointToSinglePointIntent.class);
     }
 }
diff --git a/core/pom.xml b/core/pom.xml
index c0f74cf..fc8216f 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -35,6 +35,7 @@
         <dependency>
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
index e5aa3e8..cac31c2 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
@@ -11,6 +11,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
@@ -159,6 +160,21 @@
             }
         });
 
+        clusterCommunicator.addSubscriber(GET_DEVICE_FLOW_ENTRIES, new ClusterMessageHandler() {
+
+            @Override
+            public void handle(ClusterMessage message) {
+                DeviceId deviceId = SERIALIZER.decode(message.payload());
+                log.info("Received get flow entries request for {} from {}", deviceId, message.sender());
+                Set<FlowEntry> flowEntries = getFlowEntriesInternal(deviceId);
+                try {
+                    message.respond(SERIALIZER.encode(flowEntries));
+                } catch (IOException e) {
+                    log.error("Failed to respond to peer's getFlowEntries request", e);
+                }
+            }
+        });
+
         log.info("Started");
     }
 
@@ -217,9 +233,33 @@
 
     @Override
     public synchronized Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
+
+        ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(deviceId);
+        if (replicaInfo.master().get().equals(clusterService.getLocalNode().id())) {
+            return getFlowEntriesInternal(deviceId);
+        }
+
+        log.info("Forwarding getFlowEntries to {}, which is the primary (master) for device {}",
+                replicaInfo.master().orNull(), deviceId);
+
+        ClusterMessage message = new ClusterMessage(
+                clusterService.getLocalNode().id(),
+                GET_DEVICE_FLOW_ENTRIES,
+                SERIALIZER.encode(deviceId));
+
+        try {
+            ClusterMessageResponse response = clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
+            return SERIALIZER.decode(response.get(FLOW_RULE_STORE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
+        } catch (IOException | TimeoutException e) {
+            // FIXME: throw a FlowStoreException
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Set<FlowEntry> getFlowEntriesInternal(DeviceId deviceId) {
         Collection<? extends FlowEntry> rules = flowEntries.get(deviceId);
         if (rules == null) {
-            return Collections.emptyList();
+            return Collections.emptySet();
         }
         return ImmutableSet.copyOf(rules);
     }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
index ef68b55..8f4a050 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
@@ -13,4 +13,7 @@
 
     public static final MessageSubject GET_FLOW_ENTRY
         = new MessageSubject("peer-forward-get-flow-entry");
+
+    public static final MessageSubject GET_DEVICE_FLOW_ENTRIES
+        = new MessageSubject("peer-forward-get-device-flow-entries");
 }
diff --git a/docs/external.xml b/docs/external.xml
index 959b473..45caf24 100644
--- a/docs/external.xml
+++ b/docs/external.xml
@@ -34,7 +34,7 @@
                 <version>2.10.1</version>
                 <configuration>
                     <show>package</show>
-                    <excludePackageNames>org.onlab.thirdparty:*.impl:*.impl.*:org.onlab.onos.provider.*:org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli*:org.onlab.onos.tvue:org.onlab.onos.foo:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.optical:org.onlab.onos.config:org.onlab.onos.calendar:org.onlab.onos.sdnip*:org.onlab.onos.metrics</excludePackageNames>
+                    <excludePackageNames>org.onlab.thirdparty:*.impl:*.impl.*:org.onlab.onos.provider.*:org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli*:org.onlab.onos.tvue:org.onlab.onos.foo:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.optical:org.onlab.onos.config:org.onlab.onos.calendar:org.onlab.onos.sdnip*:org.onlab.onos.metrics:org.onlab.onos.store.*:org.onlab.onos.openflow.*</excludePackageNames>
                     <docfilessubdirs>true</docfilessubdirs>
                     <doctitle>ONOS Java API</doctitle>
                     <groups>
diff --git a/pom.xml b/pom.xml
index ce02451..095e40e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -248,7 +248,7 @@
             <dependency>
                 <groupId>org.onlab.onos</groupId>
                 <artifactId>onlab-junit</artifactId>
-                <version>1.0.0-SNAPSHOT</version>
+                <version>${project.version}</version>
                 <scope>test</scope>
             </dependency>
 
diff --git a/utils/junit/pom.xml b/utils/junit/pom.xml
index d994a07..6ddadac 100644
--- a/utils/junit/pom.xml
+++ b/utils/junit/pom.xml
@@ -27,6 +27,16 @@
             <artifactId>guava-testlib</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-library</artifactId>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
similarity index 97%
rename from core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
rename to utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
index 0e63af9..30d895e 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
+++ b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
@@ -1,5 +1,4 @@
-package org.onlab.onos.net.intent;
-//TODO is this the right package?
+package org.onlab.junit;
 
 import org.hamcrest.Description;
 import org.hamcrest.StringDescription;
diff --git a/utils/misc/src/main/java/org/onlab/util/TestUtils.java b/utils/junit/src/main/java/org/onlab/junit/TestUtils.java
similarity index 99%
rename from utils/misc/src/main/java/org/onlab/util/TestUtils.java
rename to utils/junit/src/main/java/org/onlab/junit/TestUtils.java
index 7e59564..64b7ae300 100644
--- a/utils/misc/src/main/java/org/onlab/util/TestUtils.java
+++ b/utils/junit/src/main/java/org/onlab/junit/TestUtils.java
@@ -1,4 +1,4 @@
-package org.onlab.util;
+package org.onlab.junit;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
diff --git a/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java b/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java
new file mode 100644
index 0000000..bd272d1
--- /dev/null
+++ b/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java
@@ -0,0 +1,134 @@
+package org.onlab.junit;
+
+import org.hamcrest.Description;
+import org.hamcrest.StringDescription;
+import org.onlab.junit.TestUtils.TestUtilsException;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+
+/**
+ * Hamcrest style class for verifying that a class follows the
+ * accepted rules for utility classes.
+ *
+ * The rules that are enforced for utility classes:
+ *    - the class must be declared final
+ *    - the class must have only one constructor
+ *    - the constructor must be private and inaccessible to callers
+ *    - the class must have only static methods
+ */
+
+public class UtilityClassChecker {
+
+    private String failureReason = "";
+
+    /**
+     * Method to determine if a given class is a properly specified
+     * utility class.  In addition to checking that the class meets the criteria
+     * for utility classes, an object of the class type is allocated to force
+     * test code coverage onto the class constructor.
+     *
+     * @param clazz the class to check
+     * @return true if the given class is a properly specified utility class.
+     */
+    private boolean isProperlyDefinedUtilityClass(Class<?> clazz) {
+        // class must be declared final
+        if (!Modifier.isFinal(clazz.getModifiers())) {
+            failureReason = "a class that is not final";
+            return false;
+        }
+
+        // class must have only one constructor
+        final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+        if (constructors.length != 1) {
+            failureReason = "a class with more than one constructor";
+            return false;
+        }
+
+        //  constructor must not be accessible outside of the class
+        final Constructor<?> constructor = constructors[0];
+        if (constructor.isAccessible()) {
+            failureReason = "a class with an accessible default constructor";
+            return false;
+        }
+
+        // constructor must be private
+        if (!Modifier.isPrivate(constructor.getModifiers())) {
+            failureReason = "a class with a default constructor that is not private";
+            return false;
+        }
+
+        // class must have only static methods
+        for (final Method method : clazz.getMethods()) {
+            if (method.getDeclaringClass().equals(clazz)) {
+                if (!Modifier.isStatic(method.getModifiers())) {
+                    failureReason = "a class with one or more non-static methods";
+                    return false;
+                }
+            }
+
+        }
+
+        try {
+            final Object newObject = TestUtils.callConstructor(constructor);
+            if (newObject == null) {
+                failureReason = "could not instantiate a new object";
+                return false;
+            }
+        } catch (TestUtilsException e) {
+            failureReason = "could not instantiate a new object";
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Describe why an error was reported.  Uses Hamcrest style Description
+     * interfaces.
+     *
+     * @param description the Description object to use for reporting the
+     *                    mismatch
+     */
+    public void describeMismatch(Description description) {
+        description.appendText(failureReason);
+    }
+
+    /**
+     * Describe the source object that caused an error, using a Hamcrest
+     * Matcher style interface.  In this case, it always returns
+     * that we are looking for a properly defined utility class.
+     *
+     * @param description the Description object to use to report the "to"
+     *                    object
+     */
+    public void describeTo(Description description) {
+        description.appendText("a properly defined utility class");
+    }
+
+    /**
+     * Assert that the given class adheres to the utility class rules.
+     *
+     * @param clazz the class to check
+     *
+     * @throws java.lang.AssertionError if the class is not a valid
+     *         utility class
+     */
+    public static void assertThatClassIsUtility(Class<?> clazz) {
+        final UtilityClassChecker checker = new UtilityClassChecker();
+        if (!checker.isProperlyDefinedUtilityClass(clazz)) {
+            final Description toDescription = new StringDescription();
+            final Description mismatchDescription = new StringDescription();
+
+            checker.describeTo(toDescription);
+            checker.describeMismatch(mismatchDescription);
+            final String reason =
+                "\n" +
+                "Expected: is \"" + toDescription.toString() + "\"\n" +
+                "    but : was \"" + mismatchDescription.toString() + "\"";
+
+            throw new AssertionError(reason);
+        }
+    }
+}
diff --git a/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java b/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java
new file mode 100644
index 0000000..b4a6ff5
--- /dev/null
+++ b/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java
@@ -0,0 +1,120 @@
+package org.onlab.junit;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Set of unit tests to check the implementation of the immutable class
+ * checker.
+ */
+public class ImmutableClassCheckerTest {
+    /**
+     * Test class for non final class check.
+     */
+    // CHECKSTYLE IGNORE FinalClass FOR NEXT 1 LINES
+    static class NonFinal {
+        private NonFinal() { }
+    }
+
+    /**
+     * Check that a non final class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonFinalClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(NonFinal.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class FinalProtectedMember {
+        protected final int x = 0;
+    }
+
+    /**
+     * Check that a final class with a non-private member is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testFinalProtectedMember() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(FinalProtectedMember.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("a field named 'x' that is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class NotFinalPrivateMember {
+        private int x = 0;
+    }
+
+    /**
+     * Check that a final class with a non-final private
+     * member is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNotFinalPrivateMember() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(NotFinalPrivateMember.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("a field named 'x' that is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class ClassWithSetter {
+        private final int x = 0;
+        public void setX(int newX) {
+        }
+    }
+
+    /**
+     * Check that a final class with a final private
+     * member that is modifyable by a setter is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testClassWithSetter() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(ClassWithSetter.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("a class with a setter named 'setX'"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+}
+
diff --git a/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
similarity index 97%
rename from utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
rename to utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
index 58e60c1..c57b351 100644
--- a/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
+++ b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
@@ -1,4 +1,4 @@
-package org.onlab.util;
+package org.onlab.junit;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -6,7 +6,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
-import org.onlab.util.TestUtils.TestUtilsException;
+import org.onlab.junit.TestUtils.TestUtilsException;
 
 /**
  * Test and usage examples for TestUtils.
diff --git a/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java b/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java
new file mode 100644
index 0000000..7f56d81
--- /dev/null
+++ b/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java
@@ -0,0 +1,145 @@
+package org.onlab.junit;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.UtilityClassChecker.assertThatClassIsUtility;
+
+/**
+ * Set of unit tests to check the implementation of the utility class
+ * checker.
+ */
+public class UtilityClassCheckerTest {
+
+    // CHECKSTYLE:OFF test data intentionally not final
+    /**
+     * Test class for non final class check.
+     */
+    static class NonFinal {
+        private NonFinal() { }
+    }
+    // CHECKSTYLE:ON
+
+    /**
+     * Check that a non final class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonFinalClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonFinal.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for final no constructor class check.
+     */
+    static final class FinalNoConstructor {
+    }
+
+    /**
+     * Check that a final class with no declared constructor correctly produces
+     * an error.  In this case, the compiler generates a default constructor
+     * for you, but the constructor is 'protected' and will fail the check.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testFinalNoConstructorClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(FinalNoConstructor.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("class with a default constructor that " +
+                                   "is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for class with more than one constructor check.
+     */
+    static final class TwoConstructors {
+        private TwoConstructors() { }
+        private TwoConstructors(int x) { }
+    }
+
+    /**
+     * Check that a non static class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testOnlyOneConstructor() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(TwoConstructors.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("more than one constructor"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class with a non private constructor.
+     */
+    static final class NonPrivateConstructor {
+        protected NonPrivateConstructor() { }
+    }
+
+    /**
+     * Check that a class with a non private constructor correctly
+     * produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonPrivateConstructor() throws Exception {
+
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonPrivateConstructor.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("constructor that is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class with a non static method.
+     */
+    static final class NonStaticMethod {
+        private NonStaticMethod() { }
+        public void aPublicMethod() { }
+    }
+
+    /**
+     * Check that a class with a non static method correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonStaticMethod() throws Exception {
+
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonStaticMethod.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("one or more non-static methods"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+}
diff --git a/utils/misc/pom.xml b/utils/misc/pom.xml
index d1d3a50..fcb0eb8 100644
--- a/utils/misc/pom.xml
+++ b/utils/misc/pom.xml
@@ -24,6 +24,7 @@
         <dependency>
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>io.netty</groupId>
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
new file mode 100644
index 0000000..1571cf6
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
@@ -0,0 +1,202 @@
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * The class representing an IPv4 address.
+ * This class is immutable.
+ */
+public final class Ip4Address implements Comparable<Ip4Address> {
+    private final int value;
+
+    /** The length of the address in bytes (octets). */
+    public static final int BYTE_LENGTH = 4;
+
+    /** The length of the address in bits. */
+    public static final int BIT_LENGTH = BYTE_LENGTH * Byte.SIZE;
+
+    /**
+     * Default constructor.
+     */
+    public Ip4Address() {
+        this.value = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip4Address(Ip4Address other) {
+        this.value = other.value;
+    }
+
+    /**
+     * Constructor from an integer value.
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(int value) {
+        this.value = value;
+    }
+
+    /**
+     * Constructor from a byte array with the IPv4 address stored in network
+     * byte order (i.e., the most significant byte first).
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(byte[] value) {
+        this(value, 0);
+    }
+
+    /**
+     * Constructor from a byte array with the IPv4 address stored in network
+     * byte order (i.e., the most significant byte first), and a given offset
+     * from the beginning of the byte array.
+     *
+     * @param value the value to use
+     * @param offset the offset in bytes from the beginning of the byte array
+     */
+    public Ip4Address(byte[] value, int offset) {
+        checkNotNull(value);
+
+        // Verify the arguments
+        if ((offset < 0) || (offset + BYTE_LENGTH > value.length)) {
+            String msg;
+            if (value.length < BYTE_LENGTH) {
+                msg = "Invalid IPv4 address array: array length: " +
+                    value.length + ". Must be at least " + BYTE_LENGTH;
+            } else {
+                msg = "Invalid IPv4 address array: array offset: " +
+                    offset + ". Must be in the interval [0, " +
+                    (value.length - BYTE_LENGTH) + "]";
+            }
+            throw new IllegalArgumentException(msg);
+        }
+
+        // Read the address
+        ByteBuffer bb = ByteBuffer.wrap(value);
+        this.value = bb.getInt(offset);
+    }
+
+    /**
+     * Constructs an IPv4 address from a string representation of the address.
+     *<p>
+     * Example: "1.2.3.4"
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(String value) {
+        checkNotNull(value);
+
+        String[] splits = value.split("\\.");
+        if (splits.length != 4) {
+            final String msg = "Invalid IPv4 address string: " + value;
+            throw new IllegalArgumentException(msg);
+        }
+
+        int result = 0;
+        for (int i = 0; i < BYTE_LENGTH; i++) {
+            result |= Integer.parseInt(splits[i]) <<
+                ((BYTE_LENGTH - (i + 1)) * Byte.SIZE);
+        }
+        this.value = result;
+    }
+
+    /**
+     * Gets the IPv4 address as a byte array.
+     *
+     * @return a byte array with the IPv4 address stored in network byte order
+     * (i.e., the most significant byte first).
+     */
+    public byte[] toOctets() {
+        return ByteBuffer.allocate(BYTE_LENGTH).putInt(value).array();
+    }
+
+    /**
+     * Creates an IPv4 network mask prefix.
+     *
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 32].
+     * @return a new IPv4 address that contains a mask prefix of the
+     * specified length
+     */
+    public static Ip4Address makeMaskPrefix(int prefixLen) {
+        // Verify the prefix length
+        if ((prefixLen < 0) || (prefixLen > Ip4Address.BIT_LENGTH)) {
+            final String msg = "Invalid IPv4 prefix length: " + prefixLen +
+                ". Must be in the interval [0, 32].";
+            throw new IllegalArgumentException(msg);
+        }
+
+        long v =
+            (0xffffffffL << (Ip4Address.BIT_LENGTH - prefixLen)) & 0xffffffffL;
+        return new Ip4Address((int) v);
+    }
+
+    /**
+     * Creates an IPv4 address by masking it with a network mask of given
+     * mask length.
+     *
+     * @param addr the address to mask
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 32].
+     * @return a new IPv4 address that is masked with a mask prefix of the
+     * specified length
+     */
+    public static Ip4Address makeMaskedAddress(final Ip4Address addr,
+                                               int prefixLen) {
+        Ip4Address mask = Ip4Address.makeMaskPrefix(prefixLen);
+        long v = addr.value & mask.value;
+
+        return new Ip4Address((int) v);
+    }
+
+    /**
+     * Gets the value of the IPv4 address.
+     *
+     * @return the value of the IPv4 address
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Converts the IPv4 value to a '.' separated string.
+     *
+     * @return the IPv4 value as a '.' separated string
+     */
+    @Override
+    public String toString() {
+        return ((this.value >> 24) & 0xff) + "." +
+                ((this.value >> 16) & 0xff) + "." +
+                ((this.value >> 8) & 0xff) + "." +
+                (this.value & 0xff);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof Ip4Address)) {
+            return false;
+        }
+        Ip4Address other = (Ip4Address) o;
+        if (this.value != other.value) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.value;
+    }
+
+    @Override
+    public int compareTo(Ip4Address o) {
+        Long lv = ((long) this.value) & 0xffffffffL;
+        Long rv = ((long) o.value) & 0xffffffffL;
+        return lv.compareTo(rv);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
new file mode 100644
index 0000000..4d0c755
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
@@ -0,0 +1,123 @@
+package org.onlab.packet;
+
+import java.util.Objects;
+
+/**
+ * The class representing an IPv4 network address.
+ * This class is immutable.
+ */
+public final class Ip4Prefix {
+    private final Ip4Address address;           // The IPv4 address
+    private final short prefixLen;              // The prefix length
+
+    /**
+     * Default constructor.
+     */
+    public Ip4Prefix() {
+        this.address = new Ip4Address();
+        this.prefixLen = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip4Prefix(Ip4Prefix other) {
+        this.address = new Ip4Address(other.address);
+        this.prefixLen = other.prefixLen;
+    }
+
+    /**
+     * Constructor for a given address and prefix length.
+     *
+     * @param address   the address to use
+     * @param prefixLen the prefix length to use
+     */
+    public Ip4Prefix(Ip4Address address, short prefixLen) {
+        this.address = Ip4Address.makeMaskedAddress(address, prefixLen);
+        this.prefixLen = prefixLen;
+    }
+
+    /**
+     * Constructs an IPv4 prefix from a string representation of the
+     * prefix.
+     *<p>
+     * Example: "1.2.0.0/16"
+     *
+     * @param value the value to use
+     */
+    public Ip4Prefix(String value) {
+        String[] splits = value.split("/");
+        if (splits.length != 2) {
+            throw new IllegalArgumentException("Specified IPv4 prefix must contain an IPv4 " +
+                    "address and a prefix length separated by '/'");
+        }
+        this.prefixLen = Short.decode(splits[1]);
+        this.address = Ip4Address.makeMaskedAddress(new Ip4Address(splits[0]),
+                this.prefixLen);
+    }
+
+    /**
+     * Gets the address value of the IPv4 prefix.
+     *
+     * @return the address value of the IPv4 prefix
+     */
+    public Ip4Address getAddress() {
+        return address;
+    }
+
+    /**
+     * Gets the prefix length value of the IPv4 prefix.
+     *
+     * @return the prefix length value of the IPv4 prefix
+     */
+    public short getPrefixLen() {
+        return prefixLen;
+    }
+
+    /**
+     * Converts the IPv4 prefix value to an "address/prefixLen" string.
+     *
+     * @return the IPv4 prefix value as an "address/prefixLen" string
+     */
+    @Override
+    public String toString() {
+        return this.address.toString() + "/" + this.prefixLen;
+    }
+
+    /**
+     * Compares the value of two Ip4Prefix objects.
+     * <p/>
+     * Note the value of the IPv4 address is compared directly between the
+     * objects, and must match exactly for the objects to be considered equal.
+     * This may result in objects which represent the same IP prefix being
+     * classified as unequal, because the unsignificant bits of the address
+     * field don't match (the bits to the right of the prefix length).
+     * <p/>
+     * TODO Change this behavior so that objects that represent the same prefix
+     * are classified as equal according to this equals method.
+     *
+     * @see Object#equals(Object)
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+
+        if (!(other instanceof Ip4Prefix)) {
+            return false;
+        }
+
+        Ip4Prefix otherIp4Prefix = (Ip4Prefix) other;
+
+        return Objects.equals(this.address, otherIp4Prefix.address)
+                && this.prefixLen == otherIp4Prefix.prefixLen;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(address, prefixLen);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
new file mode 100644
index 0000000..57e893f
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
@@ -0,0 +1,260 @@
+package org.onlab.packet;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+import com.google.common.net.InetAddresses;
+import com.google.common.primitives.UnsignedLongs;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * The class representing an IPv6 address.
+ * This class is immutable.
+ */
+public final class Ip6Address implements Comparable<Ip6Address> {
+    private final long valueHigh;    // The higher (more significant) 64 bits
+    private final long valueLow;     // The lower (less significant) 64 bits
+
+    /** The length of the address in bytes (octets). */
+    public static final int BYTE_LENGTH = 16;
+
+    /** The length of the address in bits. */
+    public static final int BIT_LENGTH = BYTE_LENGTH * Byte.SIZE;
+
+    /**
+     * Default constructor.
+     */
+    public Ip6Address() {
+        this.valueHigh = 0;
+        this.valueLow = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip6Address(Ip6Address other) {
+        this.valueHigh = other.valueHigh;
+        this.valueLow = other.valueLow;
+    }
+
+    /**
+     * Constructor from integer values.
+     *
+     * @param valueHigh the higher (more significant) 64 bits of the address
+     * @param valueLow  the lower (less significant) 64 bits of the address
+     */
+    public Ip6Address(long valueHigh, long valueLow) {
+        this.valueHigh = valueHigh;
+        this.valueLow = valueLow;
+    }
+
+    /**
+     * Constructor from a byte array with the IPv6 address stored in network
+     * byte order (i.e., the most significant byte first).
+     *
+     * @param value the value to use
+     */
+    public Ip6Address(byte[] value) {
+        this(value, 0);
+    }
+
+    /**
+     * Constructor from a byte array with the IPv6 address stored in network
+     * byte order (i.e., the most significant byte first), and a given offset
+     * from the beginning of the byte array.
+     *
+     * @param value the value to use
+     * @param offset the offset in bytes from the beginning of the byte array
+     */
+    public Ip6Address(byte[] value, int offset) {
+        checkNotNull(value);
+
+        // Verify the arguments
+        if ((offset < 0) || (offset + BYTE_LENGTH > value.length)) {
+            String msg;
+            if (value.length < BYTE_LENGTH) {
+                msg = "Invalid IPv6 address array: array length: " +
+                    value.length + ". Must be at least " + BYTE_LENGTH;
+            } else {
+                msg = "Invalid IPv6 address array: array offset: " +
+                    offset + ". Must be in the interval [0, " +
+                    (value.length - BYTE_LENGTH) + "]";
+            }
+            throw new IllegalArgumentException(msg);
+        }
+
+        // Read the address
+        ByteBuffer bb = ByteBuffer.wrap(value);
+        bb.position(offset);
+        this.valueHigh = bb.getLong();
+        this.valueLow = bb.getLong();
+    }
+
+    /**
+     * Constructs an IPv6 address from a string representation of the address.
+     *<p>
+     * Example: "1111:2222::8888"
+     *
+     * @param value the value to use
+     */
+    public Ip6Address(String value) {
+        checkNotNull(value);
+
+        if (value.isEmpty()) {
+            final String msg = "Specified IPv6 cannot be an empty string";
+            throw new IllegalArgumentException(msg);
+        }
+        InetAddress addr = null;
+        try {
+            addr = InetAddresses.forString(value);
+        } catch (IllegalArgumentException e) {
+            final String msg = "Invalid IPv6 address string: " + value;
+            throw new IllegalArgumentException(msg);
+        }
+        byte[] bytes = addr.getAddress();
+        ByteBuffer bb = ByteBuffer.wrap(bytes);
+        this.valueHigh = bb.getLong();
+        this.valueLow = bb.getLong();
+    }
+
+    /**
+     * Gets the IPv6 address as a byte array.
+     *
+     * @return a byte array with the IPv6 address stored in network byte order
+     * (i.e., the most significant byte first).
+     */
+    public byte[] toOctets() {
+        return ByteBuffer.allocate(BYTE_LENGTH)
+            .putLong(valueHigh).putLong(valueLow).array();
+    }
+
+    /**
+     * Creates an IPv6 network mask prefix.
+     *
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 128].
+     * @return a new IPv6 address that contains a mask prefix of the
+     * specified length
+     */
+    public static Ip6Address makeMaskPrefix(int prefixLen) {
+        long vh, vl;
+
+        // Verify the prefix length
+        if ((prefixLen < 0) || (prefixLen > Ip6Address.BIT_LENGTH)) {
+            final String msg = "Invalid IPv6 prefix length: " + prefixLen +
+                ". Must be in the interval [0, 128].";
+            throw new IllegalArgumentException(msg);
+        }
+
+        if (prefixLen == 0) {
+            //
+            // NOTE: Apparently, the result of "<< 64" shifting to the left
+            // results in all 1s instead of all 0s, hence we handle it as
+            // a special case.
+            //
+            vh = 0;
+            vl = 0;
+        } else if (prefixLen <= 64) {
+            vh = (0xffffffffffffffffL << (64 - prefixLen)) & 0xffffffffffffffffL;
+            vl = 0;
+        } else {
+            vh = -1L;           // All 1s
+            vl = (0xffffffffffffffffL << (128 - prefixLen)) & 0xffffffffffffffffL;
+        }
+        return new Ip6Address(vh, vl);
+    }
+
+    /**
+     * Creates an IPv6 address by masking it with a network mask of given
+     * mask length.
+     *
+     * @param addr the address to mask
+     * @param prefixLen the length of the mask prefix. Must be in the interval
+     * [0, 128].
+     * @return a new IPv6 address that is masked with a mask prefix of the
+     * specified length
+     */
+    public static Ip6Address makeMaskedAddress(final Ip6Address addr,
+                                               int prefixLen) {
+        Ip6Address mask = Ip6Address.makeMaskPrefix(prefixLen);
+        long vh = addr.valueHigh & mask.valueHigh;
+        long vl = addr.valueLow & mask.valueLow;
+
+        return new Ip6Address(vh, vl);
+    }
+
+    /**
+     * Gets the value of the higher (more significant) 64 bits of the address.
+     *
+     * @return the value of the higher (more significant) 64 bits of the
+     * address
+     */
+    public long getValueHigh() {
+        return valueHigh;
+    }
+
+    /**
+     * Gets the value of the lower (less significant) 64 bits of the address.
+     *
+     * @return the value of the lower (less significant) 64 bits of the
+     * address
+     */
+    public long getValueLow() {
+        return valueLow;
+    }
+
+    /**
+     * Converts the IPv6 value to a ':' separated string.
+     *
+     * @return the IPv6 value as a ':' separated string
+     */
+    @Override
+    public String toString() {
+        ByteBuffer bb = ByteBuffer.allocate(Ip6Address.BYTE_LENGTH);
+        bb.putLong(valueHigh);
+        bb.putLong(valueLow);
+        InetAddress inetAddr = null;
+        try {
+            inetAddr = InetAddress.getByAddress(bb.array());
+        } catch (UnknownHostException e) {
+            // Should never happen
+            checkState(false, "Internal error: Ip6Address.toString()");
+            return "::";
+        }
+        return InetAddresses.toAddrString(inetAddr);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof Ip6Address)) {
+            return false;
+        }
+        Ip6Address other = (Ip6Address) o;
+        return this.valueHigh == other.valueHigh
+                && this.valueLow == other.valueLow;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(valueHigh, valueLow);
+    }
+
+    @Override
+    public int compareTo(Ip6Address o) {
+        // Compare the high-order 64-bit value
+        if (this.valueHigh != o.valueHigh) {
+            return UnsignedLongs.compare(this.valueHigh, o.valueHigh);
+        }
+        // Compare the low-order 64-bit value
+        if (this.valueLow != o.valueLow) {
+            return UnsignedLongs.compare(this.valueLow, o.valueLow);
+        }
+        return 0;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
new file mode 100644
index 0000000..d38f505
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
@@ -0,0 +1,123 @@
+package org.onlab.packet;
+
+import java.util.Objects;
+
+/**
+ * The class representing an IPv6 network address.
+ * This class is immutable.
+ */
+public final class Ip6Prefix {
+    private final Ip6Address address;           // The IPv6 address
+    private final short prefixLen;              // The prefix length
+
+    /**
+     * Default constructor.
+     */
+    public Ip6Prefix() {
+        this.address = new Ip6Address();
+        this.prefixLen = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip6Prefix(Ip6Prefix other) {
+        this.address = new Ip6Address(other.address);
+        this.prefixLen = other.prefixLen;
+    }
+
+    /**
+     * Constructor for a given address and prefix length.
+     *
+     * @param address   the address to use
+     * @param prefixLen the prefix length to use
+     */
+    public Ip6Prefix(Ip6Address address, short prefixLen) {
+        this.address = Ip6Address.makeMaskedAddress(address, prefixLen);
+        this.prefixLen = prefixLen;
+    }
+
+    /**
+     * Constructs an IPv6 prefix from a string representation of the
+     * prefix.
+     *<p>
+     * Example: "1111:2222::/32"
+     *
+     * @param value the value to use
+     */
+    public Ip6Prefix(String value) {
+        String[] splits = value.split("/");
+        if (splits.length != 2) {
+            throw new IllegalArgumentException("Specified IPv6 prefix must contain an IPv6 " +
+                    "address and a prefix length separated by '/'");
+        }
+        this.prefixLen = Short.decode(splits[1]);
+        this.address = Ip6Address.makeMaskedAddress(new Ip6Address(splits[0]),
+                this.prefixLen);
+    }
+
+    /**
+     * Gets the address value of the IPv6 prefix.
+     *
+     * @return the address value of the IPv6 prefix
+     */
+    public Ip6Address getAddress() {
+        return address;
+    }
+
+    /**
+     * Gets the prefix length value of the IPv6 prefix.
+     *
+     * @return the prefix length value of the IPv6 prefix
+     */
+    public short getPrefixLen() {
+        return prefixLen;
+    }
+
+    /**
+     * Converts the IPv6 prefix value to an "address/prefixLen" string.
+     *
+     * @return the IPv6 prefix value as an "address/prefixLen" string
+     */
+    @Override
+    public String toString() {
+        return this.address.toString() + "/" + this.prefixLen;
+    }
+
+    /**
+     * Compares the value of two Ip6Prefix objects.
+     * <p/>
+     * Note the value of the IPv6 address is compared directly between the
+     * objects, and must match exactly for the objects to be considered equal.
+     * This may result in objects which represent the same IP prefix being
+     * classified as unequal, because the unsignificant bits of the address
+     * field don't match (the bits to the right of the prefix length).
+     * <p/>
+     * TODO Change this behavior so that objects that represent the same prefix
+     * are classified as equal according to this equals method.
+     *
+     * @see Object#equals(Object)
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+
+        if (!(other instanceof Ip6Prefix)) {
+            return false;
+        }
+
+        Ip6Prefix otherIp6Prefix = (Ip6Prefix) other;
+
+        return Objects.equals(this.address, otherIp6Prefix.address)
+                && this.prefixLen == otherIp6Prefix.prefixLen;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(address, prefixLen);
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
new file mode 100644
index 0000000..6680af4
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
@@ -0,0 +1,330 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip4Address}.
+ */
+public class Ip4AddressTest {
+    /**
+     * Tests the immutability of {@link Ip4Address}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip4Address.class);
+    }
+
+    /**
+     * Tests the length of the address in bytes (octets).
+     */
+    @Test
+    public void testAddrBytelen() {
+        assertThat(Ip4Address.BYTE_LENGTH, is(4));
+    }
+
+    /**
+     * Tests the length of the address in bits.
+     */
+    @Test
+    public void testAddrBitlen() {
+        assertThat(Ip4Address.BIT_LENGTH, is(32));
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip4Address ip4Address = new Ip4Address();
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @Test
+    public void testCopyConstructor() {
+        Ip4Address fromAddr = new Ip4Address("1.2.3.4");
+        Ip4Address ip4Address = new Ip4Address(fromAddr);
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        fromAddr = new Ip4Address("0.0.0.0");
+        ip4Address = new Ip4Address(fromAddr);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        fromAddr = new Ip4Address("255.255.255.255");
+        ip4Address = new Ip4Address(fromAddr);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip4Address fromAddr = null;
+        Ip4Address ip4Address = new Ip4Address(fromAddr);
+    }
+
+    /**
+     * Tests valid class constructor for an integer value.
+     */
+    @Test
+    public void testConstructorForInteger() {
+        Ip4Address ip4Address = new Ip4Address(0x01020304);
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        ip4Address = new Ip4Address(0);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = new Ip4Address(0xffffffff);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests valid class constructor for an array value.
+     */
+    @Test
+    public void testConstructorForArray() {
+        final byte[] value1 = new byte[] {1, 2, 3, 4};
+        Ip4Address ip4Address = new Ip4Address(value1);
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        final byte[] value2 = new byte[] {0, 0, 0, 0};
+        ip4Address = new Ip4Address(value2);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff};
+        ip4Address = new Ip4Address(value3);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests valid class constructor for an array value and an offset.
+     */
+    @Test
+    public void testConstructorForArrayAndOffset() {
+        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
+                                          1, 2, 3, 4,
+                                          44, 55};      // Extra bytes
+        Ip4Address ip4Address = new Ip4Address(value1, 3);
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        final byte[] value2 = new byte[] {11, 22,       // Preamble
+                                          0, 0, 0, 0,
+                                          33};          // Extra bytes
+        ip4Address = new Ip4Address(value2, 2);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        final byte[] value3 = new byte[] {11, 22,       // Preamble
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          33};          // Extra bytes
+        ip4Address = new Ip4Address(value3, 2);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null array.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullArray() {
+        final byte[] fromArray = null;
+        Ip4Address ip4Address = new Ip4Address(fromArray);
+    }
+
+    /**
+     * Tests invalid class constructor for an array that is too short.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructorShortArray() {
+        final byte[] fromArray = new byte[] {1, 2, 3};
+        Ip4Address ip4Address = new Ip4Address(fromArray);
+    }
+
+    /**
+     * Tests invalid class constructor for an array and an invalid offset.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructorArrayInvalidOffset() {
+        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
+                                          1, 2, 3, 4,
+                                          44, 55};      // Extra bytes
+        Ip4Address ip4Address = new Ip4Address(value1, 6);
+    }
+
+    /**
+     * Tests valid class constructor for a string.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip4Address ip4Address = new Ip4Address(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip4Address ip4Address = new Ip4Address("");
+    }
+
+    /**
+     * Tests returning the address as a byte array.
+     */
+    @Test
+    public void testAddressToOctets() {
+        final byte[] value1 = new byte[] {1, 2, 3, 4};
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.toOctets(), is(value1));
+
+        final byte[] value2 = new byte[] {0, 0, 0, 0};
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.toOctets(), is(value2));
+
+        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff};
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.toOctets(), is(value3));
+    }
+
+    /**
+     * Tests making a mask prefix for a given prefix length.
+     */
+    @Test
+    public void testMakeMaskPrefix() {
+        Ip4Address ip4Address = Ip4Address.makeMaskPrefix(25);
+        assertThat(ip4Address.toString(), is("255.255.255.128"));
+
+        ip4Address = Ip4Address.makeMaskPrefix(0);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = Ip4Address.makeMaskPrefix(32);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests making of a masked address.
+     */
+    @Test
+    public void testMakeMaskedAddress() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.5");
+        Ip4Address ip4AddressMasked =
+            Ip4Address.makeMaskedAddress(ip4Address, 24);
+        assertThat(ip4AddressMasked.toString(), is("1.2.3.0"));
+
+        ip4AddressMasked = Ip4Address.makeMaskedAddress(ip4Address, 0);
+        assertThat(ip4AddressMasked.toString(), is("0.0.0.0"));
+
+        ip4AddressMasked = Ip4Address.makeMaskedAddress(ip4Address, 32);
+        assertThat(ip4AddressMasked.toString(), is("1.2.3.5"));
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.getValue(), is(0x01020304));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.getValue(), is(0));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.getValue(), is(-1));
+    }
+
+    /**
+     * Tests equality of {@link Ip4Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip4Address addr1 = new Ip4Address("1.2.3.4");
+        Ip4Address addr2 = new Ip4Address("1.2.3.4");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip4Address("0.0.0.0");
+        addr2 = new Ip4Address("0.0.0.0");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip4Address("255.255.255.255");
+        addr2 = new Ip4Address("255.255.255.255");
+        assertThat(addr1, is(addr2));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip4Address}.
+     */
+    @Test
+    public void testNonEquality() {
+        Ip4Address addr1 = new Ip4Address("1.2.3.4");
+        Ip4Address addr2 = new Ip4Address("1.2.3.5");
+        Ip4Address addr3 = new Ip4Address("0.0.0.0");
+        Ip4Address addr4 = new Ip4Address("255.255.255.255");
+        assertThat(addr1, is(not(addr2)));
+        assertThat(addr3, is(not(addr2)));
+        assertThat(addr4, is(not(addr2)));
+    }
+
+    /**
+     * Tests comparison of {@link Ip4Address}.
+     */
+    @Test
+    public void testComparison() {
+        Ip4Address addr1 = new Ip4Address("1.2.3.4");
+        Ip4Address addr2 = new Ip4Address("1.2.3.4");
+        Ip4Address addr3 = new Ip4Address("1.2.3.3");
+        Ip4Address addr4 = new Ip4Address("1.2.3.5");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+
+        addr1 = new Ip4Address("255.2.3.4");
+        addr2 = new Ip4Address("255.2.3.4");
+        addr3 = new Ip4Address("255.2.3.3");
+        addr4 = new Ip4Address("255.2.3.5");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
new file mode 100644
index 0000000..3e502d3
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
@@ -0,0 +1,195 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip4Prefix}.
+ */
+public class Ip4PrefixTest {
+    /**
+     * Tests the immutability of {@link Ip4Prefix}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip4Prefix.class);
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip4Prefix ip4prefix = new Ip4Prefix();
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @Test
+    public void testCopyConstructor() {
+        Ip4Prefix fromAddr = new Ip4Prefix("1.2.3.0/24");
+        Ip4Prefix ip4prefix = new Ip4Prefix(fromAddr);
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        fromAddr = new Ip4Prefix("0.0.0.0/0");
+        ip4prefix = new Ip4Prefix(fromAddr);
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        fromAddr = new Ip4Prefix("255.255.255.255/32");
+        ip4prefix = new Ip4Prefix(fromAddr);
+        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip4Prefix fromAddr = null;
+        Ip4Prefix ip4prefix = new Ip4Prefix(fromAddr);
+    }
+
+    /**
+     * Tests valid class constructor for an address and prefix length.
+     */
+    @Test
+    public void testConstructorForAddressAndPrefixLength() {
+        Ip4Prefix ip4prefix =
+            new Ip4Prefix(new Ip4Address("1.2.3.0"), (short) 24);
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix(new Ip4Address("1.2.3.4"), (short) 24);
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix(new Ip4Address("1.2.3.5"), (short) 32);
+        assertThat(ip4prefix.toString(), is("1.2.3.5/32"));
+
+        ip4prefix = new Ip4Prefix(new Ip4Address("0.0.0.0"), (short) 0);
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        ip4prefix =
+            new Ip4Prefix(new Ip4Address("255.255.255.255"), (short) 32);
+        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests valid class constructor for a string.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.4/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.5/32");
+        assertThat(ip4prefix.toString(), is("1.2.3.5/32"));
+
+        ip4prefix = new Ip4Prefix("0.0.0.0/0");
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        ip4prefix = new Ip4Prefix("255.255.255.255/32");
+        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip4Prefix ip4prefix = new Ip4Prefix(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip4Prefix ip4prefix = new Ip4Prefix("");
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
+        assertThat(ip4prefix.getAddress(), equalTo(new Ip4Address("1.2.3.0")));
+        assertThat(ip4prefix.getPrefixLen(), is((short) 24));
+
+        ip4prefix = new Ip4Prefix("0.0.0.0/0");
+        assertThat(ip4prefix.getAddress(), equalTo(new Ip4Address("0.0.0.0")));
+        assertThat(ip4prefix.getPrefixLen(), is((short) 0));
+
+        ip4prefix = new Ip4Prefix("255.255.255.255/32");
+        assertThat(ip4prefix.getAddress(),
+                   equalTo(new Ip4Address("255.255.255.255")));
+        assertThat(ip4prefix.getPrefixLen(), is((short) 32));
+    }
+
+    /**
+     * Tests equality of {@link Ip4Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip4Prefix addr1net = new Ip4Prefix("1.2.3.0/24");
+        Ip4Prefix addr2net = new Ip4Prefix("1.2.3.0/24");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip4Prefix("1.2.3.0/24");
+        addr2net = new Ip4Prefix("1.2.3.4/24");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip4Prefix("0.0.0.0/0");
+        addr2net = new Ip4Prefix("0.0.0.0/0");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip4Prefix("255.255.255.255/32");
+        addr2net = new Ip4Prefix("255.255.255.255/32");
+        assertThat(addr1net, is(addr2net));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip4Address}.
+     */
+    @Test
+    public void testNonEquality() {
+        Ip4Prefix addr1net = new Ip4Prefix("1.2.0.0/16");
+        Ip4Prefix addr2net = new Ip4Prefix("1.3.0.0/16");
+        Ip4Prefix addr3net = new Ip4Prefix("1.3.0.0/24");
+        Ip4Prefix addr4net = new Ip4Prefix("0.0.0.0/0");
+        Ip4Prefix addr5net = new Ip4Prefix("255.255.255.255/32");
+        assertThat(addr1net, is(not(addr2net)));
+        assertThat(addr3net, is(not(addr2net)));
+        assertThat(addr4net, is(not(addr2net)));
+        assertThat(addr5net, is(not(addr2net)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.4/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("0.0.0.0/0");
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        ip4prefix = new Ip4Prefix("255.255.255.255/32");
+        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
new file mode 100644
index 0000000..a053301
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
@@ -0,0 +1,437 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip6Address}.
+ */
+public class Ip6AddressTest {
+    /**
+     * Tests the immutability of {@link Ip6Address}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip6Address.class);
+    }
+
+    /**
+     * Tests the length of the address in bytes (octets).
+     */
+    @Test
+    public void testAddrBytelen() {
+        assertThat(Ip6Address.BYTE_LENGTH, is(16));
+    }
+
+    /**
+     * Tests the length of the address in bits.
+     */
+    @Test
+    public void testAddrBitlen() {
+        assertThat(Ip6Address.BIT_LENGTH, is(128));
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip6Address ip6Address = new Ip6Address();
+        assertThat(ip6Address.toString(), is("::"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @Test
+    public void testCopyConstructor() {
+        Ip6Address fromAddr =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        fromAddr = new Ip6Address("::");
+        ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(), is("::"));
+
+        fromAddr = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip6Address fromAddr = null;
+        Ip6Address ip6Address = new Ip6Address(fromAddr);
+    }
+
+    /**
+     * Tests valid class constructor for integer values.
+     */
+    @Test
+    public void testConstructorForInteger() {
+        Ip6Address ip6Address =
+            new Ip6Address(0x1111222233334444L, 0x5555666677778888L);
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ip6Address = new Ip6Address(0L, 0L);
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = new Ip6Address(-1L, -1L);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests valid class constructor for an array value.
+     */
+    @Test
+    public void testConstructorForArray() {
+        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
+                                          0x33, 0x33, 0x44, 0x44,
+                                          0x55, 0x55, 0x66, 0x66,
+                                          0x77, 0x77,
+                                          (byte) 0x88, (byte) 0x88};
+        Ip6Address ip6Address = new Ip6Address(value1);
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00};
+        ip6Address = new Ip6Address(value2);
+        assertThat(ip6Address.toString(), is("::"));
+
+        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff};
+        ip6Address = new Ip6Address(value3);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests valid class constructor for an array value and an offset.
+     */
+    @Test
+    public void testConstructorForArrayAndOffset() {
+        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
+                                          0x11, 0x11, 0x22, 0x22,
+                                          0x33, 0x33, 0x44, 0x44,
+                                          0x55, 0x55, 0x66, 0x66,
+                                          0x77, 0x77,
+                                          (byte) 0x88, (byte) 0x88,
+                                          44, 55};              // Extra bytes
+        Ip6Address ip6Address = new Ip6Address(value1, 3);
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        final byte[] value2 = new byte[] {11, 22,               // Preamble
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          33};                  // Extra bytes
+        ip6Address = new Ip6Address(value2, 2);
+        assertThat(ip6Address.toString(), is("::"));
+
+        final byte[] value3 = new byte[] {11, 22,               // Preamble
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          33};                  // Extra bytes
+        ip6Address = new Ip6Address(value3, 2);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null array.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullArray() {
+        final byte[] fromArray = null;
+        Ip6Address ip6Address = new Ip6Address(fromArray);
+    }
+
+    /**
+     * Tests invalid class constructor for an array that is too short.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructorShortArray() {
+        final byte[] fromArray = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
+        Ip6Address ip6Address = new Ip6Address(fromArray);
+    }
+
+    /**
+     * Tests invalid class constructor for an array and an invalid offset.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructorArrayInvalidOffset() {
+        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
+                                          0x11, 0x11, 0x22, 0x22,
+                                          0x33, 0x33, 0x44, 0x44,
+                                          0x55, 0x55, 0x66, 0x66,
+                                          0x77, 0x77,
+                                          (byte) 0x88, (byte) 0x88,
+                                          44, 55};              // Extra bytes
+        Ip6Address ip6Address = new Ip6Address(value1, 6);
+    }
+
+    /**
+     * Tests valid class constructor for a string.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ip6Address = new Ip6Address("::");
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip6Address ip6Address = new Ip6Address(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip6Address ip6Address = new Ip6Address("");
+    }
+
+    /**
+     * Tests returning the address as a byte array.
+     */
+    @Test
+    public void testAddressToOctets() {
+        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
+                                          0x33, 0x33, 0x44, 0x44,
+                                          0x55, 0x55, 0x66, 0x66,
+                                          0x77, 0x77,
+                                          (byte) 0x88, (byte) 0x88};
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.toOctets(), is(value1));
+
+        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00,
+                                          0x00, 0x00, 0x00, 0x00};
+        ip6Address = new Ip6Address("::");
+        assertThat(ip6Address.toOctets(), is(value2));
+
+        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff,
+                                          (byte) 0xff, (byte) 0xff};
+        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ip6Address.toOctets(), is(value3));
+    }
+
+    /**
+     * Tests making a mask prefix for a given prefix length.
+     */
+    @Test
+    public void testMakeMaskPrefix() {
+        Ip6Address ip6Address = Ip6Address.makeMaskPrefix(8);
+        assertThat(ip6Address.toString(), is("ff00::"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(120);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(0);
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(128);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(64);
+        assertThat(ip6Address.toString(), is("ffff:ffff:ffff:ffff::"));
+    }
+
+    /**
+     * Tests making of a masked address.
+     */
+    @Test
+    public void testMakeMaskedAddress() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885");
+        Ip6Address ip6AddressMasked =
+            Ip6Address.makeMaskedAddress(ip6Address, 8);
+        assertThat(ip6AddressMasked.toString(), is("1100::"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 120);
+        assertThat(ip6AddressMasked.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 0);
+        assertThat(ip6AddressMasked.toString(), is("::"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 128);
+        assertThat(ip6AddressMasked.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8885"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 64);
+        assertThat(ip6AddressMasked.toString(), is("1111:2222:3333:4444::"));
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.getValueHigh(), is(0x1111222233334444L));
+        assertThat(ip6Address.getValueLow(), is(0x5555666677778888L));
+
+        ip6Address = new Ip6Address(0, 0);
+        assertThat(ip6Address.getValueHigh(), is(0L));
+        assertThat(ip6Address.getValueLow(), is(0L));
+
+        ip6Address = new Ip6Address(-1L, -1L);
+        assertThat(ip6Address.getValueHigh(), is(-1L));
+        assertThat(ip6Address.getValueLow(), is(-1L));
+    }
+
+    /**
+     * Tests equality of {@link Ip6Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip6Address addr1 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address addr2 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip6Address("::");
+        addr2 = new Ip6Address("::");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        addr2 = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(addr1, is(addr2));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip6Address}.
+     */
+    @Test
+    public void testNonEquality() {
+        Ip6Address addr1 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address addr2 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:888A");
+        Ip6Address addr3 = new Ip6Address("::");
+        Ip6Address addr4 =
+            new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(addr1, is(not(addr2)));
+        assertThat(addr3, is(not(addr2)));
+        assertThat(addr4, is(not(addr2)));
+    }
+
+    /**
+     * Tests comparison of {@link Ip6Address}.
+     */
+    @Test
+    public void testComparison() {
+        Ip6Address addr1 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address addr2 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address addr3 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8887");
+        Ip6Address addr4 =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8889");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+
+        addr1 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr2 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr3 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8887");
+        addr4 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8889");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+
+        addr1 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr2 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr3 = new Ip6Address("ffff:2222:3333:4443:5555:6666:7777:8888");
+        addr4 = new Ip6Address("ffff:2222:3333:4445:5555:6666:7777:8888");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ip6Address = new Ip6Address("1111::8888");
+        assertThat(ip6Address.toString(), is("1111::8888"));
+
+        ip6Address = new Ip6Address("1111::");
+        assertThat(ip6Address.toString(), is("1111::"));
+
+        ip6Address = new Ip6Address("::8888");
+        assertThat(ip6Address.toString(), is("::8888"));
+
+        ip6Address = new Ip6Address("::");
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
new file mode 100644
index 0000000..dcacdc7
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
@@ -0,0 +1,255 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip6Prefix}.
+ */
+public class Ip6PrefixTest {
+    /**
+     * Tests the immutability of {@link Ip6Prefix}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip6Prefix.class);
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip6Prefix ip6prefix = new Ip6Prefix();
+        assertThat(ip6prefix.toString(), is("::/0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @Test
+    public void testCopyConstructor() {
+        Ip6Prefix fromAddr = new Ip6Prefix("1100::/8");
+        Ip6Prefix ip6prefix = new Ip6Prefix(fromAddr);
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        fromAddr = new Ip6Prefix("::/0");
+        ip6prefix = new Ip6Prefix(fromAddr);
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        fromAddr =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        ip6prefix = new Ip6Prefix(fromAddr);
+        assertThat(ip6prefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip6Prefix fromAddr = null;
+        Ip6Prefix ip6prefix = new Ip6Prefix(fromAddr);
+    }
+
+    /**
+     * Tests valid class constructor for an address and prefix length.
+     */
+    @Test
+    public void testConstructorForAddressAndPrefixLength() {
+        Ip6Prefix ip6prefix =
+            new Ip6Prefix(new Ip6Address("1100::"), (short) 8);
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
+                        (short) 8);
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8800"),
+                        (short) 120);
+        assertThat(ip6prefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ip6prefix = new Ip6Prefix(new Ip6Address("::"), (short) 0);
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
+                        (short) 128);
+        assertThat(ip6prefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8885/128"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
+                        (short) 128);
+        assertThat(ip6prefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
+                        (short) 64);
+        assertThat(ip6prefix.toString(), is("1111:2222:3333:4444::/64"));
+    }
+
+    /**
+     * Tests valid class constructor for a string.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8800/120");
+        assertThat(ip6prefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ip6prefix = new Ip6Prefix("::/0");
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/128");
+        assertThat(ip6prefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8885/128"));
+
+        ip6prefix = new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ip6prefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/64");
+        assertThat(ip6prefix.toString(), is("1111:2222:3333:4444::/64"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip6Prefix ip6prefix = new Ip6Prefix(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip6Prefix ip6prefix = new Ip6Prefix("");
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
+        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("1100::")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 8));
+
+        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("1100::")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 8));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8800/120");
+        assertThat(ip6prefix.getAddress(),
+                   equalTo(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8800")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 120));
+
+        ip6prefix = new Ip6Prefix("::/0");
+        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("::")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 0));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/128");
+        assertThat(ip6prefix.getAddress(),
+                   equalTo(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 128));
+
+        ip6prefix =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ip6prefix.getAddress(),
+                   equalTo(new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 128));
+
+        ip6prefix =
+            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/64");
+        assertThat(ip6prefix.getAddress(),
+                   equalTo(new Ip6Address("1111:2222:3333:4444::")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 64));
+    }
+
+    /**
+     * Tests equality of {@link Ip6Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip6Prefix addr1net = new Ip6Prefix("1100::/8");
+        Ip6Prefix addr2net = new Ip6Prefix("1100::/8");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        addr2net = new Ip6Prefix("1100::/8");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip6Prefix("::/0");
+        addr2net = new Ip6Prefix("::/0");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        addr2net =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(addr1net, is(addr2net));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip6Address}.
+     */
+    @Test
+    public void testNonEquality() {
+        Ip6Prefix addr1net = new Ip6Prefix("1100::/8");
+        Ip6Prefix addr2net = new Ip6Prefix("1200::/8");
+        Ip6Prefix addr3net = new Ip6Prefix("1200::/12");
+        Ip6Prefix addr4net = new Ip6Prefix("::/0");
+        Ip6Prefix addr5net =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(addr1net, is(not(addr2net)));
+        assertThat(addr3net, is(not(addr2net)));
+        assertThat(addr4net, is(not(addr2net)));
+        assertThat(addr5net, is(not(addr2net)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix = new Ip6Prefix("::/0");
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        ip6prefix =
+            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ip6prefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+}