merge
diff --git a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
index 5991e5d..bc0b425 100644
--- a/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
+++ b/apps/fwd/src/main/java/org/onlab/onos/fwd/ReactiveForwarding.java
@@ -74,13 +74,12 @@
 
         @Override
         public void process(PacketContext context) {
-            /*
-             *  stop processing if the packet has been handled,
-             *  we can't do any more to it
-             */
+            // Stop processing if the packet has been handled, since we
+            // can't do any more to it.
             if (context.isHandled()) {
                 return;
             }
+
             InboundPacket pkt = context.inPacket();
             HostId id = HostId.hostId(pkt.parsed().getDestinationMAC());
 
@@ -100,7 +99,6 @@
 
             // Otherwise, get a set of paths that lead from here to the
             // destination edge switch.
-
             Set<Path> paths = topologyService.getPaths(topologyService.currentTopology(),
                     context.inPacket().receivedFrom().deviceId(),
                     dst.location().deviceId());
@@ -137,9 +135,8 @@
 
     // Floods the specified packet.
     private void flood(PacketContext context) {
-        boolean canBcast = topologyService.isBroadcastPoint(topologyService.currentTopology(),
-                context.inPacket().receivedFrom());
-        if (canBcast) {
+        if (topologyService.isBroadcastPoint(topologyService.currentTopology(),
+                context.inPacket().receivedFrom())) {
             packetOutFlood(context);
         } else {
             context.block();
@@ -173,6 +170,10 @@
 
         flowRuleService.applyFlowRules(f);
 
+        // we don't yet support bufferids in the flowservice so packet out and
+        // then install a flowmod.
+        context.treatmentBuilder().add(Instructions.createOutput(portNumber));
+        context.send();
     }
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/HostLocation.java b/core/api/src/main/java/org/onlab/onos/net/HostLocation.java
index 22673a6..626b027 100644
--- a/core/api/src/main/java/org/onlab/onos/net/HostLocation.java
+++ b/core/api/src/main/java/org/onlab/onos/net/HostLocation.java
@@ -1,13 +1,12 @@
 package org.onlab.onos.net;
 
-import java.util.Objects;
-
 /**
  * Representation of a network edge location where an end-station host is
  * connected.
  */
 public class HostLocation extends ConnectPoint {
 
+    // Note that time is explicitly excluded from the notion of equality.
     private final long time;
 
     public HostLocation(DeviceId deviceId, PortNumber portNumber, long time) {
@@ -25,18 +24,4 @@
         return time;
     }
 
-    @Override
-    public int hashCode() {
-        return 31 * super.hashCode() + Objects.hash(time);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj instanceof HostLocation) {
-            final HostLocation other = (HostLocation) obj;
-            return super.equals(obj) && Objects.equals(this.time, other.time);
-        }
-        return false;
-    }
-
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostService.java b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
index 357fb61..a42e231 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
@@ -43,7 +43,6 @@
      * @param vlanId vlan identifier
      * @return set of hosts in the given vlan id
      */
-    // FIXME: change long to VLanId
     Set<Host> getHostsByVlan(VlanId vlanId);
 
     /**
@@ -62,6 +61,8 @@
      */
     Set<Host> getHostsByIp(IpAddress ip);
 
+    // TODO: consider adding Host getHostByIp(IpAddress ip, VlanId vlan);
+
     /**
      * Returns the set of hosts whose most recent location is the specified
      * connection point.
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/device/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/device/impl/package-info.java
index a50e32d..bb18f3a 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/device/impl/package-info.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/device/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Core subsystem for tracking infrastructure devices.
+ * Core subsystem for tracking global inventory of infrastructure devices.
  */
 package org.onlab.onos.net.trivial.device.impl;
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/flow/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/flow/impl/package-info.java
new file mode 100644
index 0000000..061c2a8
--- /dev/null
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/flow/impl/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Core subsystem for tracking and manipulating global flow state.
+ */
+package org.onlab.onos.net.trivial.flow.impl;
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/SimpleHostStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/SimpleHostStore.java
index 91c02d8..c53d2c8 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/SimpleHostStore.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/SimpleHostStore.java
@@ -48,7 +48,7 @@
      * @return appropriate event or null if no change resulted
      */
     HostEvent createOrUpdateHost(ProviderId providerId, HostId hostId,
-            HostDescription hostDescription) {
+                                 HostDescription hostDescription) {
         Host host = hosts.get(hostId);
         if (host == null) {
             return createHost(providerId, hostId, hostDescription);
@@ -58,12 +58,12 @@
 
     // creates a new host and sends HOST_ADDED
     private HostEvent createHost(ProviderId providerId, HostId hostId,
-            HostDescription descr) {
+                                 HostDescription descr) {
         DefaultHost newhost = new DefaultHost(providerId, hostId,
-                descr.hwAddress(),
-                descr.vlan(),
-                descr.location(),
-                descr.ipAddresses());
+                                              descr.hwAddress(),
+                                              descr.vlan(),
+                                              descr.location(),
+                                              descr.ipAddresses());
         synchronized (this) {
             hosts.put(hostId, newhost);
             locations.put(descr.location(), newhost);
@@ -73,23 +73,23 @@
 
     // checks for type of update to host, sends appropriate event
     private HostEvent updateHost(ProviderId providerId, Host host,
-            HostDescription descr) {
+                                 HostDescription descr) {
         DefaultHost updated;
         HostEvent event;
-        // Consider only actual location (not timestamp) change?
-        if (!(host.location().port().equals(descr.location().port()))) {
+        if (!host.location().equals(descr.location())) {
             updated = new DefaultHost(providerId, host.id(),
-                    host.mac(),
-                    host.vlan(),
-                    descr.location(),
-                    host.ipAddresses());
+                                      host.mac(),
+                                      host.vlan(),
+                                      descr.location(),
+                                      host.ipAddresses());
             event = new HostEvent(HOST_MOVED, updated);
+
         } else if (!(host.ipAddresses().equals(descr.ipAddresses()))) {
             updated = new DefaultHost(providerId, host.id(),
-                    host.mac(),
-                    host.vlan(),
-                    descr.location(),
-                    descr.ipAddresses());
+                                      host.mac(),
+                                      host.vlan(),
+                                      descr.location(),
+                                      descr.ipAddresses());
             event = new HostEvent(HOST_UPDATED, updated);
         } else {
             return null;
@@ -134,7 +134,7 @@
      * @return iterable collection of all hosts
      */
     Iterable<Host> getHosts() {
-        return Collections.unmodifiableSet(new HashSet<Host>(hosts.values()));
+        return Collections.unmodifiableSet(new HashSet<>(hosts.values()));
     }
 
     /**
@@ -154,7 +154,7 @@
      * @return set of hosts in the vlan
      */
     Set<Host> getHosts(VlanId vlanId) {
-        Set<Host> vlanset = new HashSet<Host>();
+        Set<Host> vlanset = new HashSet<>();
         for (Host h : hosts.values()) {
             if (h.vlan().equals(vlanId)) {
                 vlanset.add(h);
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/package-info.java
index 87a4a85..4dcb9ea 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/package-info.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/host/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Core subsystem for tracking edn-station hosts.
+ * Core subsystem for tracking global inventory of end-station hosts.
  */
 package org.onlab.onos.net.trivial.host.impl;
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/link/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/link/impl/package-info.java
index 09279a7..a3a2031 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/link/impl/package-info.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/link/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Core subsystem for tracking infrastructure links.
+ * Core subsystem for tracking global inventory of infrastructure links.
  */
 package org.onlab.onos.net.trivial.link.impl;
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/packet/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/packet/impl/package-info.java
index 5471948..72563a2 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/packet/impl/package-info.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/packet/impl/package-info.java
@@ -1,4 +1,6 @@
 /**
  * Core subsystem for processing inbound packets and emitting outbound packets.
+ * Processing of inbound packets is always in the local context only, but
+ * emitting outbound packets allows for cluster-wide operation.
  */
 package org.onlab.onos.net.trivial.packet.impl;
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopology.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopology.java
index ef87867..7213497 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopology.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopology.java
@@ -142,6 +142,15 @@
         return clusters.get(clusterId);
     }
 
+    /**
+     * Returns the topology cluster that contains the given device.
+     *
+     * @param deviceId device identifier
+     * @return topology cluster
+     */
+    TopologyCluster getCluster(DeviceId deviceId) {
+        return clustersByDevice.get(deviceId);
+    }
 
     /**
      * Returns the set of cluster devices.
@@ -174,13 +183,13 @@
     }
 
     /**
-     * Indicates whether the given point is part of a broadcast tree.
+     * Indicates whether the given point is part of a broadcast set.
      *
      * @param connectPoint connection point
-     * @return true if in broadcast tree
+     * @return true if in broadcast set
      */
-    boolean isInBroadcastTree(ConnectPoint connectPoint) {
-        // Any non-infrastructure, i.e. edge points are assumed to be OK
+    boolean isBroadcastPoint(ConnectPoint connectPoint) {
+        // Any non-infrastructure, i.e. edge points are assumed to be OK.
         if (!isInfrastructure(connectPoint)) {
             return true;
         }
@@ -191,13 +200,23 @@
             throw new IllegalArgumentException("No cluster found for device " + connectPoint.deviceId());
         }
 
-        // If the broadcast tree is null or empty, or if the point explicitly
-        // belongs to the broadcast tree points, return true;
+        // If the broadcast set is null or empty, or if the point explicitly
+        // belongs to it, return true;
         Set<ConnectPoint> points = broadcastSets.get(cluster.id());
         return points == null || points.isEmpty() || points.contains(connectPoint);
     }
 
     /**
+     * Returns the size of the cluster broadcast set.
+     *
+     * @param clusterId cluster identifier
+     * @return size of the cluster broadcast set
+     */
+    int broadcastSetSize(ClusterId clusterId) {
+        return broadcastSets.get(clusterId).size();
+    }
+
+    /**
      * Returns the set of pre-computed shortest paths between source and
      * destination devices.
      *
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/SimpleTopologyStore.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/SimpleTopologyStore.java
index b51ea53..59360c2 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/SimpleTopologyStore.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/SimpleTopologyStore.java
@@ -144,7 +144,7 @@
      * @return true if broadcast allowed; false otherwise
      */
     boolean isBroadcastPoint(DefaultTopology topology, ConnectPoint connectPoint) {
-        return topology.isInBroadcastTree(connectPoint);
+        return topology.isBroadcastPoint(connectPoint);
     }
 
     /**
diff --git a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/package-info.java b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/package-info.java
index 770f38d..d6d162e 100644
--- a/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/package-info.java
+++ b/core/trivial/src/main/java/org/onlab/onos/net/trivial/topology/impl/package-info.java
@@ -1,4 +1,4 @@
 /**
- * Core subsystem for tracking consistent topology graph views.
+ * Core subsystem for tracking global &amp; consistent topology graph views.
  */
 package org.onlab.onos.net.trivial.topology.impl;
diff --git a/core/trivial/src/test/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopologyTest.java b/core/trivial/src/test/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopologyTest.java
new file mode 100644
index 0000000..2520602
--- /dev/null
+++ b/core/trivial/src/test/java/org/onlab/onos/net/trivial/topology/impl/DefaultTopologyTest.java
@@ -0,0 +1,111 @@
+package org.onlab.onos.net.trivial.topology.impl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Device;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.net.topology.ClusterId;
+import org.onlab.onos.net.topology.GraphDescription;
+import org.onlab.onos.net.topology.LinkWeight;
+import org.onlab.onos.net.topology.TopologyCluster;
+import org.onlab.onos.net.topology.TopologyEdge;
+
+import java.util.Set;
+
+import static com.google.common.collect.ImmutableSet.of;
+import static org.junit.Assert.*;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.PortNumber.portNumber;
+import static org.onlab.onos.net.trivial.topology.impl.SimpleTopologyManagerTest.device;
+import static org.onlab.onos.net.trivial.topology.impl.SimpleTopologyManagerTest.link;
+
+/**
+ * Test of the default topology implementation.
+ */
+public class DefaultTopologyTest {
+
+    public static final ProviderId PID = new ProviderId("foo.bar");
+
+    public static final DeviceId D1 = deviceId("of:1");
+    public static final DeviceId D2 = deviceId("of:2");
+    public static final DeviceId D3 = deviceId("of:3");
+    public static final DeviceId D4 = deviceId("of:4");
+    public static final DeviceId D5 = deviceId("of:5");
+
+    public static final PortNumber P1 = portNumber(1);
+    public static final PortNumber P2 = portNumber(2);
+
+    public static final LinkWeight WEIGHT = new LinkWeight() {
+        @Override
+        public double weight(TopologyEdge edge) {
+            return edge.src().deviceId().equals(D4) ||
+                    edge.dst().deviceId().equals(D4) ? 2.0 : 1.0;
+        }
+    };
+
+    private DefaultTopology dt;
+
+    @Before
+    public void setUp() {
+        long now = System.currentTimeMillis();
+        Set<Device> devices = of(device("1"), device("2"),
+                                 device("3"), device("4"),
+                                 device("5"));
+        Set<Link> links = of(link("1", 1, "2", 1), link("2", 1, "1", 1),
+                             link("3", 2, "2", 2), link("2", 2, "3", 2),
+                             link("1", 3, "4", 3), link("4", 3, "1", 3),
+                             link("3", 4, "4", 4), link("4", 4, "3", 4));
+        GraphDescription graphDescription =
+                new DefaultGraphDescription(now, devices, links);
+
+        dt = new DefaultTopology(PID, graphDescription);
+        assertEquals("incorrect supplier", PID, dt.providerId());
+        assertEquals("incorrect time", now, dt.time());
+        assertEquals("incorrect device count", 5, dt.deviceCount());
+        assertEquals("incorrect link count", 8, dt.linkCount());
+        assertEquals("incorrect cluster count", 2, dt.clusterCount());
+        assertEquals("incorrect broadcast set size", 6,
+                     dt.broadcastSetSize(ClusterId.clusterId(0)));
+    }
+
+    @Test
+    public void pathRelated() {
+        Set<Path> paths = dt.getPaths(D1, D2);
+        assertEquals("incorrect path count", 1, paths.size());
+
+        paths = dt.getPaths(D1, D3);
+        assertEquals("incorrect path count", 2, paths.size());
+
+        paths = dt.getPaths(D1, D5);
+        assertTrue("no paths expected", paths.isEmpty());
+
+        paths = dt.getPaths(D1, D3, WEIGHT);
+        assertEquals("incorrect path count", 1, paths.size());
+    }
+
+    @Test
+    public void pointRelated() {
+        assertTrue("should be infrastructure point",
+                   dt.isInfrastructure(new ConnectPoint(D1, P1)));
+        assertFalse("should not be infrastructure point",
+                    dt.isInfrastructure(new ConnectPoint(D1, P2)));
+    }
+
+    @Test
+    public void clusterRelated() {
+        Set<TopologyCluster> clusters = dt.getClusters();
+        assertEquals("incorrect cluster count", 2, clusters.size());
+
+        TopologyCluster c = dt.getCluster(D1);
+        Set<DeviceId> devs = dt.getClusterDevices(c);
+        assertEquals("incorrect cluster device count", 4, devs.size());
+        assertTrue("cluster should contain D2", devs.contains(D2));
+        assertFalse("cluster should not contain D5", devs.contains(D5));
+    }
+
+}
diff --git a/of/openflowj/gen-src/main/java/org/projectfloodlight/openflow/protocol/ver13/OFActionBsnVer13.java b/of/openflowj/gen-src/main/java/org/projectfloodlight/openflow/protocol/ver13/OFActionBsnVer13.java
index cc9d8d0..02a568d 100644
--- a/of/openflowj/gen-src/main/java/org/projectfloodlight/openflow/protocol/ver13/OFActionBsnVer13.java
+++ b/of/openflowj/gen-src/main/java/org/projectfloodlight/openflow/protocol/ver13/OFActionBsnVer13.java
@@ -24,8 +24,6 @@
 import org.projectfloodlight.openflow.util.*;
 import org.projectfloodlight.openflow.exceptions.*;
 import org.jboss.netty.buffer.ChannelBuffer;
-
-import java.nio.ByteBuffer;
 import java.util.Set;
 
 abstract class OFActionBsnVer13 {
@@ -38,7 +36,7 @@
 
     static class Reader implements OFMessageReader<OFActionBsn> {
         @Override
-        public OFActionBsn readFrom(ByteBuffer bb) throws OFParseError {
+        public OFActionBsn readFrom(ChannelBuffer bb) throws OFParseError {
             if(bb.readableBytes() < MINIMUM_LENGTH)
                 return null;
             int start = bb.readerIndex();
diff --git a/providers/of/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java b/providers/of/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
index 44b0792..38ba59b 100644
--- a/providers/of/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
+++ b/providers/of/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/OpenFlowRuleProvider.java
@@ -101,7 +101,6 @@
         for (int i = 0; i < flowRules.length; i++) {
             applyRule(flowRules[i]);
         }
-
     }
 
     private void applyRule(FlowRule flowRule) {
@@ -120,9 +119,7 @@
                 .setHardTimeout(10)
                 .setPriority(flowRule.priority())
                 .build();
-
         sw.sendMsg(fm);
-
     }
 
     private List<OFAction> buildActions(List<Instruction> instructions, OFFactory factory) {
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index d4aa64a..a9852ba 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -12,7 +12,7 @@
 
 # Setup a path
 export PS=":"
-export PATH="$PATH:$ONOS_ROOT/tools/dev;$ONOS_ROOT/tools/package"
+export PATH="$PATH:$ONOS_ROOT/tools/dev:$ONOS_ROOT/tools/package"
 export PATH="$PATH:$MAVEN/bin:$KARAF/bin"
 export PATH="$PATH:."
 
diff --git a/tools/package/package b/tools/package/package
index 870acc6..0bf8a39 100755
--- a/tools/package/package
+++ b/tools/package/package
@@ -29,18 +29,13 @@
 # Unroll the Apache Karaf bits and make the ONOS top-level directories.
 unzip $KARAF_ZIP
 mkdir bin
-mkdir lib
 
 # Stage the ONOS admin scripts
 cp -r $ONOS_ROOT/tools/package/bin .
 
 # Stage the ONOS bundles
-mkdir -p lib/org/onlab 
-cp -r $M2_REPO/org/onlab lib/org
-
-
-# Patch the Apache Karaf distribution file to point to the lib as maven repo
-#perl -pi.old -e "s|^org.ops4j.pax.url.mvn.repositories= |org.ops4j.pax.url.mvn.repositories= \\\n    file:../../lib, |" $ONOS_STAGE/$KARAF_DIST/etc/org.ops4j.pax.url.mvn.cfg
+mkdir -p system/org/onlab 
+cp -r $M2_REPO/org/onlab system/org/
 
 # Patch the Apache Karaf distribution file to add ONOS features repository
 perl -pi.old -e "s|^(featuresRepositories=.*)|\1,mvn:org.onlab.onos/onos-features/$ONOS_VERSION/xml/features|" \
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
index c664a3a..109672c 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
@@ -5,7 +5,7 @@
 /**
  * A class representing an IPv4 address.
  */
-public class IpAddress {
+public final class IpAddress {
 
     //IP Versions
     public enum Version { INET, INET6 };
@@ -14,13 +14,30 @@
     public static final int INET_LEN = 4;
     public static final int INET6_LEN = 16;
 
-    protected Version version;
-    //does it make more sense to have a integral address?
-    protected byte[] octets;
+    //maximum CIDR value
+    public static final int MAX_INET_MASK = 32;
+    public static final int DEFAULT_MASK = 0;
 
-    protected IpAddress(Version ver, byte[] octets) {
+    /**
+     * Default value indicating an unspecified address.
+     */
+    public static final byte [] ANY = new byte [] {0, 0, 0, 0};
+
+    protected Version version;
+
+    protected byte[] octets;
+    protected int netmask;
+
+    private IpAddress(Version ver, byte[] octets, int netmask) {
         this.version = ver;
         this.octets = Arrays.copyOf(octets, INET_LEN);
+        this.netmask = netmask;
+    }
+
+    private IpAddress(Version ver, byte[] octets) {
+        this.version = ver;
+        this.octets = Arrays.copyOf(octets, INET_LEN);
+        this.netmask = DEFAULT_MASK;
     }
 
     /**
@@ -34,38 +51,87 @@
     }
 
     /**
+     * Converts a byte array into an IP address.
+     *
+     * @param address a byte array
+     * @param netmask the CIDR value subnet mask
+     * @return an IP address
+     */
+    public static IpAddress valueOf(byte [] address, int netmask) {
+        return new IpAddress(Version.INET, address, netmask);
+    }
+
+    /**
+     * Helper to convert an integer into a byte array.
+     *
+     * @param address the integer to convert
+     * @return a byte array
+     */
+    private static byte [] bytes(int address) {
+        byte [] bytes = new byte [INET_LEN];
+        for (int i = 0; i < INET_LEN; i++) {
+            bytes[i] = (byte) ((address >> (INET_LEN - (i + 1)) * 8) & 0xff);
+        }
+
+        return bytes;
+    }
+
+    /**
      * Converts an integer into an IPv4 address.
      *
      * @param address an integer representing an IP value
      * @return an IP address
      */
     public static IpAddress valueOf(int address) {
-        byte [] bytes = new byte [INET_LEN];
-        for (int i = 0; i < INET_LEN; i++) {
-            bytes[i] = (byte) ((address >> (INET_LEN - (i + 1)) * 8) & 0xff);
-        }
-
-        return new IpAddress(Version.INET, bytes);
+        return new IpAddress(Version.INET, bytes(address));
     }
 
     /**
-     * Converts a string in dotted-decimal notation (x.x.x.x) into
-     * an IPv4 address.
+     * Converts an integer into an IPv4 address.
      *
-     * @param address a string representing an IP address, e.g. "10.0.0.1"
+     * @param address an integer representing an IP value
+     * @param netmask the CIDR value subnet mask
+     * @return an IP address
+     */
+    public static IpAddress valueOf(int address, int netmask) {
+        return new IpAddress(Version.INET, bytes(address), netmask);
+    }
+
+    /**
+     * Converts a dotted-decimal string (x.x.x.x) into an IPv4 address. The
+     * string can also be in CIDR (slash) notation.
+     *
+     * @param address a IP address in string form, e.g. "10.0.0.1", "10.0.0.1/24"
      * @return an IP address
      */
     public static IpAddress valueOf(String address) {
-        final String [] parts = address.split("\\.");
-        if (parts.length != INET_LEN) {
+
+        final String [] parts = address.split("\\/");
+        if (parts.length > 2) {
+            throw new IllegalArgumentException("Malformed IP address string; "
+                    + "Addres must take form \"x.x.x.x\" or \"x.x.x.x/y\"");
+        }
+
+        int mask = DEFAULT_MASK;
+        if (parts.length == 2) {
+            mask = Integer.valueOf(parts[1]);
+            if (mask > MAX_INET_MASK) {
+                throw new IllegalArgumentException(
+                        "Value of subnet mask cannot exceed "
+                        + MAX_INET_MASK);
+            }
+        }
+
+        final String [] net = parts[0].split("\\.");
+        if (net.length != INET_LEN) {
             throw new IllegalArgumentException("Malformed IP address string; "
                     + "Addres must have four decimal values separated by dots (.)");
         }
         final byte [] bytes = new byte[INET_LEN];
         for (int i = 0; i < INET_LEN; i++) {
-            bytes[i] = Byte.parseByte(parts[i], 10);
+            bytes[i] = (byte) Short.parseShort(net[i], 10);
         }
-        return new IpAddress(Version.INET, bytes);
+        return new IpAddress(Version.INET, bytes, mask);
     }
 
     /**
@@ -99,34 +165,122 @@
         return address;
     }
 
+    /**
+     * Helper for computing the mask value from CIDR.
+     *
+     * @return an integer bitmask
+     */
+    private int mask() {
+        int shift = MAX_INET_MASK - this.netmask;
+        return ((Integer.MAX_VALUE >>> (shift - 1)) << shift);
+    }
+
+    /**
+     * Returns the subnet mask in IpAddress form. The netmask value for
+     * the returned IpAddress is 0, as the address itself is a mask.
+     *
+     * @return the subnet mask
+     */
+    public IpAddress netmask() {
+        return new IpAddress(Version.INET, bytes(mask()));
+    }
+
+    /**
+     * Returns the network portion of this address as an IpAddress.
+     * The netmask of the returned IpAddress is the current mask. If this
+     * address doesn't have a mask, this returns an all-0 IpAddress.
+     *
+     * @return the network address or null
+     */
+    public IpAddress network() {
+        if (netmask == DEFAULT_MASK) {
+            return new IpAddress(version, ANY, DEFAULT_MASK);
+        }
+
+        byte [] net = new byte [4];
+        byte [] mask = bytes(mask());
+        for (int i = 0; i < INET_LEN; i++) {
+             net[i] = (byte) (octets[i] & mask[i]);
+        }
+        return new IpAddress(version, net, netmask);
+    }
+
+    /**
+     * Returns the host portion of the IPAddress, as an IPAddress.
+     * The netmask of the returned IpAddress is the current mask. If this
+     * address doesn't have a mask, this returns a copy of the current
+     * address.
+     *
+     * @return the host address
+     */
+    public IpAddress host() {
+        if (netmask == DEFAULT_MASK) {
+            new IpAddress(version, octets, netmask);
+        }
+
+        byte [] host = new byte [INET_LEN];
+        byte [] mask = bytes(mask());
+        for (int i = 0; i < INET_LEN; i++) {
+             host[i] = (byte) (octets[i] & ~mask[i]);
+        }
+        return new IpAddress(version, host, netmask);
+    }
+
     @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + netmask;
+        result = prime * result + Arrays.hashCode(octets);
+        result = prime * result + ((version == null) ? 0 : version.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        IpAddress other = (IpAddress) obj;
+        if (netmask != other.netmask) {
+            return false;
+        }
+        if (!Arrays.equals(octets, other.octets)) {
+            return false;
+        }
+        if (version != other.version) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    /*
+     * (non-Javadoc)
+     * format is "x.x.x.x" for non-masked (netmask 0) addresses,
+     * and "x.x.x.x/y" for masked addresses.
+     *
+     * @see java.lang.Object#toString()
+     */
     public String toString() {
         final StringBuilder builder = new StringBuilder();
         for (final byte b : this.octets) {
             if (builder.length() > 0) {
                 builder.append(".");
             }
-            builder.append(String.format("%d", b));
+            builder.append(String.format("%d", b & 0xff));
+        }
+        if (netmask != DEFAULT_MASK) {
+            builder.append("/");
+            builder.append(String.format("%d", netmask));
         }
         return builder.toString();
     }
 
-    @Override
-    public int hashCode() {
-        return Arrays.hashCode(octets);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-
-        if (obj instanceof IpAddress) {
-            IpAddress other = (IpAddress) obj;
-
-            if (this.version.equals(other.version)
-                    && (Arrays.equals(this.octets, other.octets))) {
-                return true;
-            }
-        }
-        return false;
-    }
 }
diff --git a/utils/misc/src/test/java/org/onlab/packet/IPAddressTest.java b/utils/misc/src/test/java/org/onlab/packet/IPAddressTest.java
index a0757cd..f1a7b0d 100644
--- a/utils/misc/src/test/java/org/onlab/packet/IPAddressTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/IPAddressTest.java
@@ -1,6 +1,7 @@
 package org.onlab.packet;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.util.Arrays;
 
@@ -11,33 +12,65 @@
 
 public class IPAddressTest {
 
-    private static final byte [] BYTES1 = new byte [] {0x0, 0x0, 0x0, 0xa};
-    private static final byte [] BYTES2 = new byte [] {0x0, 0x0, 0x0, 0xb};
-    private static final int INTVAL1 = 10;
-    private static final int INTVAL2 = 12;
-    private static final String STRVAL = "0.0.0.11";
+    private static final byte [] BYTES1 = new byte [] {0xa, 0x0, 0x0, 0xa};
+    private static final byte [] BYTES2 = new byte [] {0xa, 0x0, 0x0, 0xb};
+    private static final int INTVAL1 = 167772170;
+    private static final int INTVAL2 = 167772171;
+    private static final String STRVAL = "10.0.0.12";
+    private static final int MASK = 16;
 
     @Test
     public void testEquality() {
         IpAddress ip1 = IpAddress.valueOf(BYTES1);
-        IpAddress ip2 = IpAddress.valueOf(BYTES2);
-        IpAddress ip3 = IpAddress.valueOf(INTVAL1);
+        IpAddress ip2 = IpAddress.valueOf(INTVAL1);
+        IpAddress ip3 = IpAddress.valueOf(BYTES2);
         IpAddress ip4 = IpAddress.valueOf(INTVAL2);
         IpAddress ip5 = IpAddress.valueOf(STRVAL);
 
-        new EqualsTester().addEqualityGroup(ip1, ip3)
-        .addEqualityGroup(ip2, ip5)
-        .addEqualityGroup(ip4)
+        new EqualsTester().addEqualityGroup(ip1, ip2)
+        .addEqualityGroup(ip3, ip4)
+        .addEqualityGroup(ip5)
         .testEquals();
+
+        // string conversions
+        IpAddress ip6 = IpAddress.valueOf(BYTES1, MASK);
+        IpAddress ip7 = IpAddress.valueOf("10.0.0.10/16");
+        IpAddress ip8 = IpAddress.valueOf(new byte [] {0xa, 0x0, 0x0, 0xc});
+        assertEquals("incorrect address conversion", ip6, ip7);
+        assertEquals("incorrect address conversion", ip5, ip8);
     }
 
     @Test
     public void basics() {
-        IpAddress ip4 = IpAddress.valueOf(BYTES1);
-        assertEquals("incorrect IP Version", Version.INET, ip4.version());
-        assertEquals("faulty toOctets()", Arrays.equals(
-                new byte [] {0x0, 0x0, 0x0, 0xa}, ip4.toOctets()), true);
-        assertEquals("faulty toInt()", INTVAL1, ip4.toInt());
-        assertEquals("faulty toString()", "0.0.0.10", ip4.toString());
+        IpAddress ip1 = IpAddress.valueOf(BYTES1, MASK);
+        final byte [] bytes = new byte [] {0xa, 0x0, 0x0, 0xa};
+
+        //check fields
+        assertEquals("incorrect IP Version", Version.INET, ip1.version());
+        assertEquals("incorrect netmask", 16, ip1.netmask);
+        assertTrue("faulty toOctets()", Arrays.equals(bytes, ip1.toOctets()));
+        assertEquals("faulty toInt()", INTVAL1, ip1.toInt());
+        assertEquals("faulty toString()", "10.0.0.10/16", ip1.toString());
+    }
+
+    @Test
+    public void netmasks() {
+        // masked
+        IpAddress ip1 = IpAddress.valueOf(BYTES1, MASK);
+
+        IpAddress host = IpAddress.valueOf("0.0.0.10/16");
+        IpAddress network = IpAddress.valueOf("10.0.0.0/16");
+        assertEquals("incorrect host address", host, ip1.host());
+        assertEquals("incorrect network address", network, ip1.network());
+        assertEquals("incorrect netmask", "255.255.0.0", ip1.netmask().toString());
+
+        //unmasked
+        IpAddress ip2 = IpAddress.valueOf(BYTES1);
+        IpAddress umhost = IpAddress.valueOf("10.0.0.10/0");
+        IpAddress umnet = IpAddress.valueOf("0.0.0.0/0");
+        assertEquals("incorrect host address", umhost, ip2.host());
+        assertEquals("incorrect host address", umnet, ip2.network());
+        assertTrue("incorrect netmask",
+                Arrays.equals(IpAddress.ANY, ip2.netmask().toOctets()));
     }
 }