[CORD-638] ICMPv6 NDP support

Changes:
- Adds the support for the ND protocol;
- Changes in several places Ip4Address and Ip4Prefix for general objects;

Change-Id: I7429b8f4acc9ffe432b49b66e66da50045996f7c
diff --git a/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java b/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
index 80b233b..9ac2b26 100644
--- a/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
+++ b/src/main/java/org/onosproject/segmentrouting/IcmpHandler.java
@@ -19,19 +19,27 @@
 import org.onlab.packet.ICMP;
 import org.onlab.packet.IPv4;
 import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip6Address;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MPLS;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
 import org.onosproject.incubator.net.neighbour.NeighbourMessageContext;
+import org.onosproject.incubator.net.neighbour.NeighbourMessageType;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostService;
 import org.onosproject.net.packet.DefaultOutboundPacket;
 import org.onosproject.net.packet.InboundPacket;
 import org.onosproject.net.packet.OutboundPacket;
 import org.onosproject.segmentrouting.config.DeviceConfigNotFoundException;
 import org.onosproject.segmentrouting.config.DeviceConfiguration;
+import org.onosproject.segmentrouting.config.SegmentRoutingAppConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,6 +68,10 @@
         this.config = checkNotNull(srManager.deviceConfiguration);
     }
 
+    //////////////////////////////////////
+    //     ICMP Echo/Reply Protocol     //
+    //////////////////////////////////////
+
     /**
      * Process incoming ICMP packet.
      * If it is an ICMP request to router or known host, then sends an ICMP response.
@@ -112,19 +124,6 @@
     }
 
     /**
-     * Process incoming ICMP packet.
-     * If it is an ICMP request to router or known host, then sends an ICMP response.
-     * If it is an ICMP packet to known host and forward the packet to the host.
-     * If it is an ICMP packet to unknown host in a subnet, then sends an ARP request
-     * to the subnet.
-     *
-     * @param pkt inbound packet
-     */
-    public void processPacketIn(NeighbourMessageContext pkt) {
-
-    }
-
-    /**
      * Sends an ICMP reply message.
      *
      * Note: we assume that packets sending from the edge switches to the hosts
@@ -202,5 +201,208 @@
         }
     }
 
+    ///////////////////////////////////////////
+    //  ICMPv6 Neighbour Discovery Protocol  //
+    ///////////////////////////////////////////
 
+    /**
+     * Process incoming NDP packet.
+     *
+     * If it is an NDP request for the router or for the gateway, then sends a NDP reply.
+     * If it is an NDP request to unknown host flood in the subnet.
+     * If it is an NDP packet to known host forward the packet to the host.
+     *
+     * FIXME If the NDP packets use link local addresses we fail.
+     *
+     * @param pkt inbound packet
+     * @param hostService the host service
+     */
+    public void processPacketIn(NeighbourMessageContext pkt, HostService hostService) {
+        /*
+         * First we validate the ndp packet
+         */
+        SegmentRoutingAppConfig appConfig = srManager.cfgService
+                .getConfig(srManager.appId, SegmentRoutingAppConfig.class);
+        if (appConfig != null && appConfig.suppressSubnet().contains(pkt.inPort())) {
+            // Ignore NDP packets come from suppressed ports
+            pkt.drop();
+            return;
+        }
+        if (!validateSrcIp(pkt)) {
+            log.debug("Ignore NDP packet discovered on {} with unexpected src ip address {}.",
+                      pkt.inPort(), pkt.sender());
+            pkt.drop();
+            return;
+        }
+
+        if (pkt.type() == NeighbourMessageType.REQUEST) {
+            handleNdpRequest(pkt, hostService);
+        } else {
+            handleNdpReply(pkt, hostService);
+        }
+
+    }
+
+    /**
+     * Utility function to verify if the src ip belongs to the same
+     * subnet configured on the port it is seen.
+     *
+     * @param pkt the ndp packet and context information
+     * @return true if the src ip is a valid address for the subnet configured
+     * for the connect point
+     */
+    private boolean validateSrcIp(NeighbourMessageContext pkt) {
+        ConnectPoint connectPoint = pkt.inPort();
+        IpPrefix subnet = config.getPortIPv6Subnet(
+                connectPoint.deviceId(),
+                connectPoint.port()
+        ).getIp6Prefix();
+        return subnet != null && subnet.contains(pkt.sender());
+    }
+
+    /**
+     * Helper method to handle the ndp requests.
+     *
+     * @param pkt the ndp packet request and context information
+     * @param hostService the host service
+     */
+    private void handleNdpRequest(NeighbourMessageContext pkt, HostService hostService) {
+        /*
+         * ND request for the gateway. We have to reply on behalf
+         * of the gateway.
+         */
+        if (isNdpForGateway(pkt)) {
+            log.debug("Sending NDP reply on behalf of the router");
+            sendNdpReply(pkt, config.getRouterMacForAGatewayIp(pkt.target()));
+        } else {
+            /*
+             * ND request for an host. We do a search by Ip.
+             */
+            Set<Host> hosts = hostService.getHostsByIp(pkt.target());
+            /*
+             * Possible misconfiguration ? In future this case
+             * should be handled we can have same hosts in different
+             * vlans.
+             */
+            if (hosts.size() > 1) {
+                log.warn("More than one host with IP {}", pkt.target());
+            }
+            Host targetHost = hosts.stream().findFirst().orElse(null);
+            /*
+             * If we know the host forward to its attachment
+             * point.
+             */
+            if (targetHost != null) {
+                log.debug("Forward NDP request to the target host");
+                pkt.forward(targetHost.location());
+            } else {
+                /*
+                 * Flood otherwise.
+                 */
+                log.debug("Flood NDP request to the target subnet");
+                flood(pkt);
+            }
+        }
+    }
+
+    /**
+     * Helper method to handle the ndp replies.
+     *
+     * @param pkt the ndp packet reply and context information
+     * @param hostService the host service
+     */
+    private void handleNdpReply(NeighbourMessageContext pkt, HostService hostService) {
+        if (isNdpForGateway(pkt)) {
+            log.debug("Forwarding all the ip packets we stored");
+            Ip6Address hostIpAddress = pkt.sender().getIp6Address();
+            srManager.ipHandler.forwardPackets(pkt.inPort().deviceId(), hostIpAddress);
+        } else {
+            HostId hostId = HostId.hostId(pkt.dstMac(), pkt.vlan());
+            Host targetHost = hostService.getHost(hostId);
+            if (targetHost != null) {
+                log.debug("Forwarding the reply to the host");
+                pkt.forward(targetHost.location());
+            } else {
+                /*
+                 * We don't have to flood towards spine facing ports.
+                 */
+                if (pkt.vlan().equals(VlanId.vlanId(SegmentRoutingManager.ASSIGNED_VLAN_NO_SUBNET))) {
+                    return;
+                }
+                log.debug("Flooding the reply to the subnet");
+                flood(pkt);
+            }
+        }
+    }
+
+    /**
+     * Utility to verify if the ND are for the gateway.
+     *
+     * @param pkt the ndp packet
+     * @return true if the ndp is for the gateway. False otherwise
+     */
+    private boolean isNdpForGateway(NeighbourMessageContext pkt) {
+        DeviceId deviceId = pkt.inPort().deviceId();
+        Set<IpAddress> gatewayIpAddresses = null;
+        try {
+            if (pkt.target().equals(config.getRouterIpv6(deviceId))) {
+                return true;
+            }
+            gatewayIpAddresses = config.getPortIPs(deviceId);
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage() + " Aborting check for router IP in processing ndp");
+        }
+        if (gatewayIpAddresses != null &&
+                gatewayIpAddresses.contains(pkt.target())) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Utility to send a ND reply using the supplied information.
+     *
+     * @param pkt the ndp request
+     * @param targetMac the target mac
+     */
+    private void sendNdpReply(NeighbourMessageContext pkt, MacAddress targetMac) {
+        HostId dstId = HostId.hostId(pkt.srcMac(), pkt.vlan());
+        Host dst = srManager.hostService.getHost(dstId);
+        if (dst == null) {
+            log.warn("Cannot send NDP response to host {} - does not exist in the store",
+                     dstId);
+            return;
+        }
+        pkt.reply(targetMac);
+    }
+
+    /*
+     * Floods only on the port which have been configured with the subnet
+     * of the target address. The in port is excluded.
+     *
+     * @param pkt the ndp packet and context information
+     */
+    private void flood(NeighbourMessageContext pkt) {
+        try {
+            srManager.deviceConfiguration
+                    .getSubnetPortsMap(pkt.inPort().deviceId())
+                    .forEach((subnet, ports) -> {
+                        if (subnet.contains(pkt.target())) {
+                            ports.stream()
+                                    .filter(portNumber -> portNumber != pkt.inPort().port())
+                                    .forEach(portNumber -> {
+                                        ConnectPoint outPoint = new ConnectPoint(
+                                                pkt.inPort().deviceId(),
+                                                portNumber
+                                        );
+                                        pkt.forward(outPoint);
+                                    });
+                        }
+                    });
+        } catch (DeviceConfigNotFoundException e) {
+            log.warn(e.getMessage()
+                             + " Cannot flood in subnet as device config not available"
+                             + " for device: " + pkt.inPort().deviceId());
+        }
+    }
 }