Support RADIUS server outside of the ONOS network

Change-Id: I7e64faae6831467e084db878e02023d40fb33f07
diff --git a/src/main/java/org/onosproject/aaa/AAA.java b/src/main/java/org/onosproject/aaa/AAA.java
index 479ec7e..caa9800 100644
--- a/src/main/java/org/onosproject/aaa/AAA.java
+++ b/src/main/java/org/onosproject/aaa/AAA.java
@@ -15,10 +15,13 @@
  */
 package org.onosproject.aaa;
 
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
-import java.util.Optional;
-import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -31,19 +34,14 @@
 import org.onlab.packet.EthType;
 import org.onlab.packet.Ethernet;
 import org.onlab.packet.IPv4;
-import org.onlab.packet.Ip4Address;
-import org.onlab.packet.IpAddress;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.RADIUS;
 import org.onlab.packet.RADIUSAttribute;
 import org.onlab.packet.TpPort;
-import org.onlab.packet.UDP;
-import org.onlab.packet.VlanId;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DeviceId;
-import org.onosproject.net.Host;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.config.ConfigFactory;
 import org.onosproject.net.config.NetworkConfigEvent;
@@ -53,7 +51,6 @@
 import org.onosproject.net.flow.DefaultTrafficTreatment;
 import org.onosproject.net.flow.TrafficSelector;
 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;
@@ -63,6 +60,8 @@
 import org.onosproject.xosintegration.VoltTenantService;
 import org.slf4j.Logger;
 
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
 import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
 import static org.onosproject.net.packet.PacketPriority.CONTROL;
 import static org.slf4j.LoggerFactory.getLogger;
@@ -85,10 +84,6 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected PacketService packetService;
 
-    // end host information
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected HostService hostService;
-
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected VoltTenantService voltTenantService;
 
@@ -121,6 +116,12 @@
     // our unique identifier
     private ApplicationId appId;
 
+    // Socket used for UDP communications with RADIUS server
+    private DatagramSocket radiusSocket;
+
+    // Executor for RADIUS communication thread
+    private ExecutorService executor;
+
     // Configuration properties factory
     private final ConfigFactory factory =
             new ConfigFactory<ApplicationId, AAAConfig>(APP_SUBJECT_FACTORY,
@@ -184,7 +185,16 @@
 
         StateMachine.initializeMaps();
 
-        hostService.startMonitoringIp(IpAddress.valueOf(radiusIpAddress));
+        try {
+            radiusSocket = new DatagramSocket(radiusServerPort);
+        } catch (Exception ex) {
+            log.error("Can't open RADIUS socket", ex);
+        }
+
+        executor = Executors.newSingleThreadExecutor(
+                new ThreadFactoryBuilder()
+                        .setNameFormat("AAA-radius-%d").build());
+        executor.execute(radiusListener);
     }
 
     @Deactivate
@@ -195,6 +205,24 @@
         packetService.removeProcessor(processor);
         processor = null;
         StateMachine.destroyMaps();
+        radiusSocket.close();
+        executor.shutdownNow();
+    }
+
+    protected void sendRADIUSPacket(RADIUS radiusPacket) {
+
+        try {
+            final byte[] data = radiusPacket.serialize();
+            final DatagramSocket socket = radiusSocket;
+
+            DatagramPacket packet =
+                    new DatagramPacket(data, data.length,
+                                       radiusIpAddress, radiusServerPort);
+
+            socket.send(packet);
+        } catch (IOException e) {
+            log.info("Cannot send packet to RADIUS server", e);
+        }
     }
 
     /**
@@ -232,6 +260,19 @@
         packetService.cancelPackets(radSelector, CONTROL, appId);
     }
 
+    /**
+     * Send the ethernet packet to the supplicant.
+     *
+     * @param ethernetPkt  the ethernet packet
+     * @param connectPoint the connect point to send out
+     */
+    private void sendPacketToSupplicant(Ethernet ethernetPkt, ConnectPoint connectPoint) {
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder().setOutput(connectPoint.port()).build();
+        OutboundPacket packet = new DefaultOutboundPacket(connectPoint.deviceId(),
+                                                          treatment, ByteBuffer.wrap(ethernetPkt.serialize()));
+        packetService.emit(packet);
+    }
+
     // our handler defined as a private inner class
 
     /**
@@ -253,26 +294,11 @@
                     case EAPOL:
                         handleSupplicantPacket(context.inPacket());
                         break;
-                    case IPV4:
-                        IPv4 ipv4Packet = (IPv4) ethPkt.getPayload();
-                        Ip4Address srcIp = Ip4Address.valueOf(ipv4Packet.getSourceAddress());
-                        Ip4Address radiusIp4Address = Ip4Address.valueOf(radiusIpAddress);
-                        if (srcIp.equals(radiusIp4Address) && ipv4Packet.getProtocol() == IPv4.PROTOCOL_UDP) {
-                            // TODO: check for port as well when it's configurable
-                            UDP udpPacket = (UDP) ipv4Packet.getPayload();
-
-                            byte[] datagram = udpPacket.getPayload().serialize();
-                            RADIUS radiusPacket;
-                            radiusPacket = RADIUS.deserializer().deserialize(datagram, 0, datagram.length);
-                            handleRadiusPacket(radiusPacket);
-                        }
-
-                        break;
                     default:
                         log.trace("Skipping Ethernet packet type {}",
                                   EthType.EtherType.lookup(ethPkt.getEtherType()));
                 }
-            } catch (DeserializationException | StateMachineException e) {
+            } catch (StateMachineException e) {
                 log.warn("Unable to process RADIUS packet:", e);
             }
         }
@@ -280,23 +306,26 @@
         /**
          * Creates and initializes common fields of a RADIUS packet.
          *
-         * @param identifier RADIUS identifier
+         * @param stateMachine state machine for the request
          * @param eapPacket  EAP packet
          * @return RADIUS packet
          */
-        private RADIUS getRadiusPayload(byte identifier, EAP eapPacket) {
+        private RADIUS getRadiusPayload(StateMachine stateMachine, byte identifier, EAP eapPacket) {
             RADIUS radiusPayload =
                     new RADIUS(RADIUS.RADIUS_CODE_ACCESS_REQUEST,
                                eapPacket.getIdentifier());
+
+            // set Request Authenticator in StateMachine
+            stateMachine.setRequestAuthenticator(radiusPayload.generateAuthCode());
+
             radiusPayload.setIdentifier(identifier);
             radiusPayload.setAttribute(RADIUSAttribute.RADIUS_ATTR_USERNAME,
-                                       eapPacket.getData());
+                                       stateMachine.username());
 
             radiusPayload.setAttribute(RADIUSAttribute.RADIUS_ATTR_NAS_IP,
                                        AAA.this.nasIpAddress.getAddress());
 
             radiusPayload.encapsulateMessage(eapPacket);
-            radiusPayload.addMessageAuthenticator(AAA.this.radiusSecret);
 
             return radiusPayload;
         }
@@ -329,13 +358,13 @@
 
                     //send an EAP Request/Identify to the supplicant
                     EAP eapPayload = new EAP(EAP.REQUEST, stateMachine.identifier(), EAP.ATTR_IDENTITY, null);
-                    Ethernet eth = buildEapolResponse(srcMAC, MacAddress.valueOf(1L),
+                    Ethernet eth = buildEapolResponse(srcMAC, MacAddress.valueOf(nasMacAddress),
                                                       ethPkt.getVlanID(), EAPOL.EAPOL_PACKET,
                                                       eapPayload);
                     stateMachine.setSupplicantAddress(srcMAC);
                     stateMachine.setVlanId(ethPkt.getVlanID());
 
-                    this.sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
+                    sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
 
                     break;
                 case EAPOL.EAPOL_PACKET:
@@ -350,11 +379,10 @@
                             // request id access to RADIUS
                             stateMachine.setUsername(eapPacket.getData());
 
-                            radiusPayload = getRadiusPayload(stateMachine.identifier(), eapPacket);
+                            radiusPayload = getRadiusPayload(stateMachine, stateMachine.identifier(), eapPacket);
+                            radiusPayload.addMessageAuthenticator(AAA.this.radiusSecret);
 
-                            // set Request Authenticator in StateMachine
-                            stateMachine.setRequestAuthenticator(radiusPayload.generateAuthCode());
-                            sendRadiusMessage(radiusPayload);
+                            sendRADIUSPacket(radiusPayload);
 
                             // change the state to "PENDING"
                             stateMachine.requestAccess();
@@ -365,22 +393,27 @@
                             // machine.
                             if (eapPacket.getIdentifier() == stateMachine.challengeIdentifier()) {
                                 //send the RADIUS challenge response
-                                radiusPayload = getRadiusPayload(stateMachine.challengeIdentifier(), eapPacket);
+                                radiusPayload =
+                                        getRadiusPayload(stateMachine,
+                                                         stateMachine.identifier(),
+                                                         eapPacket);
 
                                 radiusPayload.setAttribute(RADIUSAttribute.RADIUS_ATTR_STATE,
                                                            stateMachine.challengeState());
-                                sendRadiusMessage(radiusPayload);
+                                radiusPayload.addMessageAuthenticator(AAA.this.radiusSecret);
+                                sendRADIUSPacket(radiusPayload);
                             }
                             break;
                         case EAP.ATTR_TLS:
                             // request id access to RADIUS
-                            radiusPayload = getRadiusPayload(stateMachine.identifier(), eapPacket);
+                            radiusPayload = getRadiusPayload(stateMachine, stateMachine.identifier(), eapPacket);
 
                             radiusPayload.setAttribute(RADIUSAttribute.RADIUS_ATTR_STATE,
                                                        stateMachine.challengeState());
                             stateMachine.setRequestAuthenticator(radiusPayload.generateAuthCode());
 
-                            sendRadiusMessage(radiusPayload);
+                            sendRADIUSPacket(radiusPayload);
+                            radiusPayload.addMessageAuthenticator(AAA.this.radiusSecret);
                             // TODO: this gets called on every fragment, should only be called at TLS-Start
                             stateMachine.requestAccess();
 
@@ -392,14 +425,18 @@
                 default:
                     log.trace("Skipping EAPOL message {}", eapol.getEapolType());
             }
+
         }
+    }
+
+    class RadiusListener implements Runnable {
 
         /**
          * Handles RADIUS packets.
          *
          * @param radiusPacket RADIUS packet coming from the RADIUS server.
          */
-        private void handleRadiusPacket(RADIUS radiusPacket) throws StateMachineException {
+        protected void handleRadiusPacket(RADIUS radiusPacket) throws StateMachineException {
             StateMachine stateMachine = StateMachine.lookupStateMachineById(radiusPacket.getIdentifier());
             if (stateMachine == null) {
                 log.error("Invalid session identifier, exiting...");
@@ -410,13 +447,16 @@
             Ethernet eth;
             switch (radiusPacket.getCode()) {
                 case RADIUS.RADIUS_CODE_ACCESS_CHALLENGE:
-                    byte[] challengeState = radiusPacket.getAttribute(RADIUSAttribute.RADIUS_ATTR_STATE).getValue();
+                    byte[] challengeState =
+                            radiusPacket.getAttribute(RADIUSAttribute.RADIUS_ATTR_STATE).getValue();
                     eapPayload = radiusPacket.decapsulateMessage();
                     stateMachine.setChallengeInfo(eapPayload.getIdentifier(), challengeState);
                     eth = buildEapolResponse(stateMachine.supplicantAddress(),
-                                             MacAddress.valueOf(1L), stateMachine.vlanId(), EAPOL.EAPOL_PACKET,
+                                             MacAddress.valueOf(nasMacAddress),
+                                             stateMachine.vlanId(),
+                                             EAPOL.EAPOL_PACKET,
                                              eapPayload);
-                    this.sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
+                    sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
                     break;
                 case RADIUS.RADIUS_CODE_ACCESS_ACCEPT:
                     //send an EAPOL - Success to the supplicant.
@@ -425,9 +465,11 @@
                     eapPayload = new EAP();
                     eapPayload = (EAP) eapPayload.deserialize(eapMessage, 0, eapMessage.length);
                     eth = buildEapolResponse(stateMachine.supplicantAddress(),
-                                             MacAddress.valueOf(1L), stateMachine.vlanId(), EAPOL.EAPOL_PACKET,
+                                             MacAddress.valueOf(nasMacAddress),
+                                             stateMachine.vlanId(),
+                                             EAPOL.EAPOL_PACKET,
                                              eapPayload);
-                    this.sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
+                    sendPacketToSupplicant(eth, stateMachine.supplicantConnectpoint());
 
                     stateMachine.authorizeAccess();
                     break;
@@ -439,59 +481,43 @@
             }
         }
 
-        private void sendRadiusMessage(RADIUS radiusMessage) {
-            Set<Host> hosts = hostService.getHostsByIp(IpAddress.valueOf(radiusIpAddress));
-            Optional<Host> odst = hosts.stream().filter(h -> h.vlan().toShort() == VlanId.UNTAGGED).findFirst();
 
-            Host dst;
-            if (!odst.isPresent()) {
-                log.info("Radius server {} is not present", radiusIpAddress);
-                return;
-            } else {
-                dst = odst.get();
+        @Override
+        public void run() {
+            boolean done = false;
+            int packetNumber = 1;
+
+            log.info("UDP listener thread starting up");
+            RADIUS inboundRadiusPacket;
+            while (!done) {
+                try {
+                    DatagramPacket inboundBasePacket = new DatagramPacket(new byte[1000], 1000);
+                    DatagramSocket socket = radiusSocket;
+                    socket.receive(inboundBasePacket);
+                    log.info("Packet #{} received", packetNumber++);
+                    try {
+                        inboundRadiusPacket =
+                                RADIUS.deserializer()
+                                        .deserialize(inboundBasePacket.getData(),
+                                                     0,
+                                                     inboundBasePacket.getLength());
+                        handleRadiusPacket(inboundRadiusPacket);
+                    } catch (DeserializationException dex) {
+                        log.error("Cannot deserialize packet", dex);
+                    } catch (StateMachineException sme) {
+                        log.error("Illegal state machine operation", sme);
+                    }
+
+                } catch (IOException e) {
+                    log.info("Socket was closed, exiting listener thread");
+                    done = true;
+                }
             }
-
-            UDP udp = new UDP();
-            IPv4 ip4Packet = new IPv4();
-            Ethernet ethPkt = new Ethernet();
-            radiusMessage.setParent(udp);
-            udp.setDestinationPort(radiusServerPort);
-            udp.setSourcePort(radiusServerPort);
-            udp.setPayload(radiusMessage);
-            udp.setParent(ip4Packet);
-            ip4Packet.setSourceAddress(AAA.this.nasIpAddress.getHostAddress());
-            ip4Packet.setDestinationAddress(AAA.this.radiusIpAddress.getHostAddress());
-            ip4Packet.setProtocol(IPv4.PROTOCOL_UDP);
-            ip4Packet.setPayload(udp);
-            ip4Packet.setParent(ethPkt);
-            ethPkt.setDestinationMACAddress(radiusMacAddress);
-            ethPkt.setSourceMACAddress(nasMacAddress);
-            ethPkt.setEtherType(Ethernet.TYPE_IPV4);
-            ethPkt.setPayload(ip4Packet);
-
-            TrafficTreatment treatment = DefaultTrafficTreatment.builder()
-                    .setOutput(PortNumber.portNumber(radiusPort)).build();
-            OutboundPacket packet = new DefaultOutboundPacket(DeviceId.deviceId(radiusSwitch),
-                                                              treatment, ByteBuffer.wrap(ethPkt.serialize()));
-            packetService.emit(packet);
-
         }
-
-        /**
-         * Send the ethernet packet to the supplicant.
-         *
-         * @param ethernetPkt  the ethernet packet
-         * @param connectPoint the connect point to send out
-         */
-        private void sendPacketToSupplicant(Ethernet ethernetPkt, ConnectPoint connectPoint) {
-            TrafficTreatment treatment = DefaultTrafficTreatment.builder().setOutput(connectPoint.port()).build();
-            OutboundPacket packet = new DefaultOutboundPacket(connectPoint.deviceId(),
-                                                              treatment, ByteBuffer.wrap(ethernetPkt.serialize()));
-            packetService.emit(packet);
-        }
-
     }
 
+    RadiusListener radiusListener = new RadiusListener();
+
     private class InternalConfigListener implements NetworkConfigListener {
 
         /**