VOL-149: Configure RADIUS Attributes as per subscriber information
VOL-148: RADIUS VLAN ID configurable

Change-Id: I2a51dbf316637e685b7e3c36595a27922c79b23c
diff --git a/src/main/java/org/opencord/aaa/PortBasedRadiusCommunicator.java b/src/main/java/org/opencord/aaa/PortBasedRadiusCommunicator.java
new file mode 100755
index 0000000..6e256b7
--- /dev/null
+++ b/src/main/java/org/opencord/aaa/PortBasedRadiusCommunicator.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.opencord.aaa;
+
+import org.onlab.packet.ARP;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.EthType;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.RADIUS;
+import org.onlab.packet.TpPort;
+import org.onlab.packet.UDP;
+
+import org.onosproject.core.ApplicationId;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipListener;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketService;
+
+import org.opencord.sadis.SubscriberAndDeviceInformation;
+import org.opencord.sadis.SubscriberAndDeviceInformationService;
+
+import org.slf4j.Logger;
+
+import com.google.common.collect.Maps;
+
+import static org.onosproject.net.packet.PacketPriority.CONTROL;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handles communication with the RADIUS server through ports
+ * of the SDN switches.
+ */
+public class PortBasedRadiusCommunicator implements RadiusCommunicator {
+
+    // for verbose output
+    private final Logger log = getLogger(getClass());
+
+    // our unique identifier
+    private ApplicationId appId;
+
+    // to receive Packet-in events that we'll respond to
+    PacketService packetService;
+
+    DeviceService deviceService;
+
+    MastershipService mastershipService;
+
+    SubscriberAndDeviceInformationService subsService;
+
+    // to store local mapping of IP Address and Serial No of Device
+    private Map<Ip4Address, String> ipToSnMap;
+
+    // connect points to the RADIUS server
+    Set<ConnectPoint> radiusConnectPoints;
+
+    // Parsed RADIUS server addresses
+    protected InetAddress radiusIpAddress;
+
+    // RADIUS server TCP port number
+    protected short radiusServerPort;
+
+    protected String radiusMacAddress;
+
+    // NAS IP address
+    protected InetAddress nasIpAddress;
+
+    protected String nasMacAddress;
+
+    // RADIUS server Vlan ID
+    private short radiusVlanID;
+
+    // RADIUS p-bit
+    private byte radiusPBit;
+
+    PacketCustomizer pktCustomizer;
+    AaaManager aaaManager;
+
+    ConnectPoint radiusServerConnectPoint = null;
+
+    InnerMastershipListener changeListener = new InnerMastershipListener();
+    InnerDeviceListener deviceListener = new InnerDeviceListener();
+
+    PortBasedRadiusCommunicator(ApplicationId appId, PacketService pktService,
+                                MastershipService masService, DeviceService devService,
+                                SubscriberAndDeviceInformationService subsService,
+                                PacketCustomizer pktCustomizer, AaaManager aaaManager) {
+        this.appId = appId;
+        this.packetService = pktService;
+        this.mastershipService = masService;
+        this.deviceService = devService;
+        this.subsService = subsService;
+        this.pktCustomizer = pktCustomizer;
+        this.aaaManager = aaaManager;
+
+        ipToSnMap = Maps.newConcurrentMap();
+        mastershipService.addListener(changeListener);
+        deviceService.addListener(deviceListener);
+
+        log.error("Created PortBased");
+    }
+
+    private void initializeLocalState() {
+        synchronized (this) {
+            radiusServerConnectPoint = null;
+            if (radiusConnectPoints != null) {
+                // find a connect point through a device for which we are master
+                for (ConnectPoint cp: radiusConnectPoints) {
+                    if (mastershipService.isLocalMaster(cp.deviceId())) {
+                        if (deviceService.isAvailable(cp.deviceId())) {
+                            radiusServerConnectPoint = cp;
+                        }
+                        log.warn("RADIUS connectPoint selected is {}", cp);
+                        break;
+                    }
+                }
+            }
+
+            log.warn("RADIUS connectPoint in initializeLocalState is {}", radiusServerConnectPoint);
+
+            if (radiusServerConnectPoint == null) {
+                log.error("Master of none, can't send radius Message to server");
+            }
+        }
+    }
+
+    @Override
+    public void initializeLocalState(AaaConfig newCfg) {
+        if (newCfg.nasIp() != null) {
+            nasIpAddress = newCfg.nasIp();
+        }
+        if (newCfg.radiusIp() != null) {
+            radiusIpAddress = newCfg.radiusIp();
+        }
+        if (newCfg.radiusMac() != null) {
+            radiusMacAddress = newCfg.radiusMac();
+        }
+        if (newCfg.nasMac() != null) {
+            nasMacAddress = newCfg.nasMac();
+        }
+
+        radiusServerPort = newCfg.radiusServerUdpPort();
+        radiusVlanID = newCfg.radiusServerVlanId();
+        radiusPBit = newCfg.radiusServerPBit();
+
+        radiusConnectPoints = newCfg.radiusServerConnectPoints();
+
+        initializeLocalState();
+    }
+
+    @Override
+    public void clearLocalState() {}
+
+    @Override
+    public void deactivate() {
+        mastershipService.removeListener(changeListener);
+        deviceService.removeListener(deviceListener);
+    }
+
+    @Override
+    public void requestIntercepts() {
+        TrafficSelector.Builder selectorArpServer = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_ARP);
+        packetService.requestPackets(selectorArpServer.build(), CONTROL, appId);
+
+        TrafficSelector.Builder selectorServer = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPProtocol(IPv4.PROTOCOL_UDP)
+                .matchUdpSrc(TpPort.tpPort(radiusServerPort));
+        packetService.requestPackets(selectorServer.build(), CONTROL, appId);
+    }
+
+    @Override
+    public void withdrawIntercepts() {
+        TrafficSelector.Builder selectorArpServer = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_ARP);
+        packetService.cancelPackets(selectorArpServer.build(), CONTROL, appId);
+
+        TrafficSelector.Builder selectorServer = DefaultTrafficSelector.builder()
+                .matchEthType(Ethernet.TYPE_IPV4)
+                .matchIPProtocol(IPv4.PROTOCOL_UDP)
+                .matchUdpSrc(TpPort.tpPort(radiusServerPort));
+        packetService.cancelPackets(selectorServer.build(), CONTROL, appId);
+    }
+
+    @Override
+    public void sendRadiusPacket(RADIUS radiusPacket, InboundPacket inPkt) {
+        // create the packet
+        Ethernet ethReply = new Ethernet();
+        ethReply.setSourceMACAddress(nasMacAddress);
+        ethReply.setDestinationMACAddress(radiusMacAddress);
+        ethReply.setEtherType(Ethernet.TYPE_IPV4);
+        ethReply.setVlanID(radiusVlanID);
+        ethReply.setPriorityCode(radiusPBit);
+
+        IPv4 ipv4Packet = new IPv4();
+        ipv4Packet.setTtl((byte) 64);
+        ipv4Packet.setSourceAddress(Ip4Address.
+                valueOf(nasIpAddress).toInt());
+        ipv4Packet.setDestinationAddress(Ip4Address.
+                valueOf(radiusIpAddress).toInt());
+
+        UDP udpPacket = new UDP();
+        udpPacket.setSourcePort(radiusServerPort);
+        udpPacket.setDestinationPort(radiusServerPort);
+
+        udpPacket.setPayload(radiusPacket);
+        ipv4Packet.setPayload(udpPacket);
+        ethReply.setPayload(ipv4Packet);
+
+        // store the IP address and SN of the device, later to be used
+        // for ARP responses
+        String serialNo = deviceService.getDevice(inPkt.
+                receivedFrom().deviceId()).serialNumber();
+
+        SubscriberAndDeviceInformation deviceInfo = subsService.get(serialNo);
+
+        if (deviceInfo == null) {
+            log.error("No Device found with SN {}", serialNo);
+            return;
+        }
+        ipToSnMap.put(deviceInfo.ipAddress(), serialNo);
+
+        // send the message out
+        sendFromRadiusServerPort(pktCustomizer.
+                customizeEthernetIPHeaders(ethReply, inPkt));
+    }
+
+    /**
+     * Sends packet to the RADIUS server using one of the switch ports.
+     *
+     * @param packet Ethernet packet to be sent
+     */
+    private void sendFromRadiusServerPort(Ethernet packet) {
+        if (radiusServerConnectPoint != null) {
+            log.trace("AAA Manager sending Ethernet packet = {}", packet);
+            TrafficTreatment t = DefaultTrafficTreatment.builder()
+                    .setOutput(radiusServerConnectPoint.port()).build();
+            OutboundPacket o = new DefaultOutboundPacket(
+                    radiusServerConnectPoint.deviceId(), t, ByteBuffer.wrap(packet.serialize()));
+            packetService.emit(o);
+        } else {
+            log.error("Unable to send RADIUS packet, connectPoint is null");
+        }
+    }
+
+    @Override
+    public void handlePacketFromServer(PacketContext context) {
+        // Extract the original Ethernet frame from the packet information
+        InboundPacket pkt = context.inPacket();
+        Ethernet ethPkt = pkt.parsed();
+        if (ethPkt == null) {
+            return;
+        }
+
+        // identify if incoming packet
+        switch (EthType.EtherType.lookup(ethPkt.getEtherType())) {
+            case ARP:
+                handleArpPacketFromServer(context);
+                break;
+            case IPV4:
+                handleIPv4PacketFromServer(context);
+                break;
+            default:
+                log.debug("Skipping Ethernet packet type {}",
+                        EthType.EtherType.lookup(ethPkt.getEtherType()));
+        }
+    }
+
+    /**
+     * Handles ARP packets from RADIUS server.
+     *
+     * @param context Context for the packet
+     */
+    private void handleArpPacketFromServer(PacketContext context) {
+        // Extract the original Ethernet frame from the packet information
+        InboundPacket pkt = context.inPacket();
+        Ethernet ethPkt = pkt.parsed();
+        if (ethPkt == null) {
+            return;
+        }
+
+        ARP arpPacket = (ARP) ethPkt.getPayload();
+
+        Ip4Address targetAddress = Ip4Address.valueOf(arpPacket.
+                getTargetProtocolAddress());
+
+        String serialNo = ipToSnMap.get(targetAddress);
+        if (serialNo == null) {
+            log.info("No mapping found for ARP reply, target address {}",
+                    targetAddress);
+            return;
+        }
+        MacAddress senderMac = subsService.get(serialNo).hardwareIdentifier();
+        if (senderMac == null) {
+            log.error("ARP resolution, MAC address not found for SN {}", serialNo);
+            return;
+        }
+
+        ARP arpReply = (ARP) arpPacket.clone();
+        arpReply.setOpCode(ARP.OP_REPLY);
+        arpReply.setTargetProtocolAddress(arpPacket.getSenderProtocolAddress());
+        arpReply.setTargetHardwareAddress(arpPacket.getSenderHardwareAddress());
+        arpReply.setSenderProtocolAddress(arpPacket.getTargetProtocolAddress());
+        arpReply.setSenderHardwareAddress(senderMac.toBytes());
+
+        log.debug("AAA Manager: Query for ARP of IP : {}", arpPacket.getTargetProtocolAddress());
+
+        // Ethernet Frame.
+        Ethernet ethReply = new Ethernet();
+        ethReply.setSourceMACAddress(senderMac);
+        ethReply.setDestinationMACAddress(ethPkt.getSourceMAC());
+        ethReply.setEtherType(Ethernet.TYPE_ARP);
+        ethReply.setVlanID(radiusVlanID);
+        ethReply.setPriorityCode(ethPkt.getPriorityCode());
+
+        ethReply.setPayload(arpReply);
+        sendFromRadiusServerPort(ethReply);
+    }
+
+    /**
+     * Handles IP packets from RADIUS server.
+     *
+     * @param context Context for the packet
+     */
+    private void handleIPv4PacketFromServer(PacketContext context) {
+        // Extract the original Ethernet frame from the packet information
+        InboundPacket pkt = context.inPacket();
+        Ethernet ethPkt = pkt.parsed();
+        if (ethPkt == null) {
+            return;
+        }
+
+        IPv4 ipv4Packet = (IPv4) ethPkt.getPayload();
+
+        if (ipv4Packet.getProtocol() == IPv4.PROTOCOL_UDP) {
+            UDP udpPacket = (UDP) ipv4Packet.getPayload();
+
+            if (udpPacket.getSourcePort() == radiusServerPort) {
+                //This packet is RADIUS packet from the server.
+                RADIUS radiusMsg;
+                try {
+                    radiusMsg =
+                            RADIUS.deserializer()
+                                    .deserialize(udpPacket.serialize(),
+                                            8,
+                                            udpPacket.getLength() - 8);
+                    try {
+                        aaaManager.handleRadiusPacket(radiusMsg);
+                    }  catch (StateMachineException sme) {
+                        log.error("Illegal state machine operation", sme);
+                    }
+                } catch (DeserializationException dex) {
+                    log.error("Cannot deserialize packet", dex);
+                }
+            }
+        }
+    }
+
+    /**
+     * Handles Mastership changes for the devices which connect
+     * to the RADIUS server.
+     */
+    private class InnerMastershipListener implements MastershipListener {
+        @Override
+        public void event(MastershipEvent event) {
+            if (radiusServerConnectPoint != null &&
+                    radiusServerConnectPoint.deviceId().
+                            equals(event.subject())) {
+                log.trace("Mastership Event recevived for {}", event.subject());
+                // mastership of the device for our connect point has changed
+                // reselect
+                initializeLocalState();
+            }
+        }
+    }
+
+    /**
+     * Handles Device status change for the devices which connect
+     * to the RADIUS server.
+     */
+    private class InnerDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            log.trace("Device Event recevived for {} event {}", event.subject(), event.type());
+            if (radiusServerConnectPoint == null) {
+                switch (event.type()) {
+                    case DEVICE_ADDED:
+                    case DEVICE_AVAILABILITY_CHANGED:
+                        // some device is available check if we can get one
+                        initializeLocalState();
+                        break;
+                    default:
+                        break;
+                }
+                return;
+            }
+            if (radiusServerConnectPoint.deviceId().
+                    equals(event.subject().id())) {
+                switch (event.type()) {
+                    case DEVICE_AVAILABILITY_CHANGED:
+                    case DEVICE_REMOVED:
+                    case DEVICE_SUSPENDED:
+                        // state of our device has changed, check if we need
+                        // to re-select
+                        initializeLocalState();
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+    }
+}