content: /* The contents of this file are subject to the terms
   * of the Common Development and Distribution License
   * (the License). You may not use this file except in
   * compliance with the License.
   *
   * You can obtain a copy of the License at
   * http://www.sun.com/cddl/cddl.html or
   * install_dir/legal/LICENSE
   * See the License for the specific language governing
   * permission and limitations under the License.
   *
   * When distributing Covered Code, include this CDDL
   * Header Notice in each file and include the License file
   * at install_dir/legal/LICENSE.
   * If applicable, add the following below the CDDL Header,
   * with the fields enclosed by brackets [] replaced by
   * your own identifying information:
   * "Portions Copyrighted [year] [name of copyright owner]"
   *
   * $Id$
   *
   * Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
   */
  package com.sun.faban.harness.util;
  
  import com.sun.faban.harness.ConfigurationException;
  import java.util.concurrent.*;
  import java.util.*;
  import java.util.logging.Logger;
  import java.util.logging.Level;
  import java.net.*;
  import java.io.IOException;
  
  /**
   * The InterfaceProbe probes interfaces and finds the best interface to
   * communicate with an IP address or host name.
   */
  public class InterfaceProbe {
  
      Logger logger = Logger.getLogger(this.getClass().getName());
      ExecutorService threadPool = null;
      static final int TIMEOUT = 10000;
      static final int LOCAL_TIMEOUT = 5000;
  
      // Parallel probing is still buggy, especially for ICMP echo (root ping)
      // mode. So we'll not use it just yet. This seems like a JDK bug.
      // Once we get it working in all instances, we'll change this.
      static final int PARALLEL_THRESHOLD = Integer.MAX_VALUE;
  
      ArrayList<NetworkInterface> ifList;
      List<IFAddressInfo> ifAInfoList;
  
      /**
       * Unit tests the interface probe.
       * @param args The commmand line arguments
       */
      public static void main(String[] args) {
          try {
              ArrayList<String> hosts = new ArrayList<String>();
              for (String arg : args) {
                  hosts.add(arg);
              }
              InterfaceProbe ifs = new InterfaceProbe();
              List<Route> rtes = ifs.getRoutes(hosts);
              System.out.println("destination\tiface\tifAddress\tttl\treachable");
              for (Route rte : rtes) {
                  System.out.print(rte.host + '\t');
                  if (rte.nif != null)
                      System.out.print(rte.nif.getDisplayName() + '\t');
                  else
                      System.out.print("-\t");
                  if (rte.nifAddress != null)
                      System.out.print(rte.nifAddress);
                  else
                      System.out.print("-\t");
                  System.out.println("\t" + rte.ttl + '\t' + rte.reachable);
              }
              System.exit(0);
          } catch (SocketException e) {
              e.printStackTrace();
          }
          System.exit(1);
      }
  
      static class Route {
          String host;
          InetAddress target;
          NetworkInterface nif;
          String nifAddress;
          short ttl = 0;
          boolean reachable = false;
      }
  
      static class IFAddressInfo {
          NetworkInterface nif;
          InterfaceAddress nifAddress;
          short prefixLength;
          byte[] netAddress;
      }
  
      /**
       * .
       * @throws java.net.SocketException
       */
      public InterfaceProbe() throws SocketException {
          Enumeration<NetworkInterface> interfaces =
                  NetworkInterface.getNetworkInterfaces();
          ifList = new ArrayList<NetworkInterface>();
          addInterfaces(interfaces, ifList);
          ifAInfoList = listIFAddressInfo(ifList);
      }
  
      /**
       * .
       * @param executor
       * @throws java.net.SocketException
       */
      public InterfaceProbe(ExecutorService executor) throws SocketException {
          this();
          threadPool = executor;
      }
  
      /**
       * Sets the thread pool to be used for parallel probing.
       * @param executor The thread pool
       */
      public void setExecutorService(ExecutorService executor) {
          threadPool = executor;
      }
  
      private List<IFAddressInfo> listIFAddressInfo(
                                              List<NetworkInterface> ifList) {
          ArrayList<IFAddressInfo> ifAInfos = new ArrayList<IFAddressInfo>();
          ifSearchLoop:
          for (NetworkInterface nif : ifList) {
              List<InterfaceAddress> ifAddresses =
                      nif.getInterfaceAddresses();
              for (InterfaceAddress ifAddress : ifAddresses) {
                  if (ifAddress == null)
                      continue;
                  IFAddressInfo ifAInfo = new IFAddressInfo();
                  ifAInfo.nif = nif;
                  ifAInfo.nifAddress = ifAddress;
                  InetAddress ia = ifAddress.getAddress();
                  ifAInfo.prefixLength = ifAddress.getNetworkPrefixLength();
                  try {
                      ifAInfo.netAddress =
                              getNetworkAddress(ia, ifAInfo.prefixLength);
                  } catch (ConfigurationException e) {
                      StringBuilder buffer = new StringBuilder();
                      buffer.append(e.getMessage()).append('\n');
                      buffer = printIfInfo(ifAInfo, buffer);
                      logger.log(Level.WARNING, buffer.toString(), e);
                      continue;
                  }
                  ifAInfos.add(ifAInfo);
                  printIfInfo(ifAInfo);
              }
          }
          return ifAInfos;
      }
  
      /**
       * Fills the interface maps identifying which interface is to be used
       * to communicate to a particular host.
       * @param hosts The host list
       * @param ifMap The host to interface map to fill
       * @return The host to interface map provided, filled in
       */
      public Map<String, String> getIfMap(Collection<String> hosts,
                                          Map<String, String> ifMap) {
  
          // Grab only the hosts without interfaces from ifMap.
          HashSet<String> hostsToProbe = new HashSet<String>();
          for (String host : hosts) {
              String iface = ifMap.get(host);
              if (iface == null || iface.length() == 0)
                  hostsToProbe.add(host);
          }
  
          // Obtain the routes
          List<Route> routes = getRoutes(hostsToProbe);
  
          // Complete the ifMap
          for (Route route : routes)
              ifMap.put(route.host, route.nifAddress);
          return ifMap;
      }
  
      /**
       * Obtains the route list for a list of hosts.
       * @param hosts The list of hosts
       * @return the corresponding list of routes
       */
      List<Route> getRoutes(Collection<String> hosts) {
          ArrayList<Route> routes = new ArrayList<Route>();
          if (hosts.size() < PARALLEL_THRESHOLD) {
              for (String host : hosts) {
                  Route rte = getOrFindRoute(host);
                  routes.add(rte);
              }
          } else {
              if (threadPool == null)
                  threadPool = Executors.newCachedThreadPool();
              ArrayList<Future<Route>> futures = new ArrayList<Future<Route>>();
              for (String target : hosts)
                  futures.add(threadPool.submit(new GetRouteTask(target)));
              for (Future<Route> f : futures) {
                  try {
                      routes.add(f.get());
                  } catch (InterruptedException e) {
                      logger.log(Level.WARNING, "Route scan interrupted", e);
                  } catch (ExecutionException e) {
                      Throwable e2 = e.getCause();
                      logger.log(Level.WARNING, e2.getMessage(), e2);
                  }
              }
          }
          return routes;
      }
  
      private Route getOrFindRoute(String host) {
          Route rte = new Route();
          rte.host = host;
          try {
              rte.target = InetAddress.getByName(host);
          } catch (UnknownHostException e) {
              logger.log(Level.WARNING, "Host " + host + " unknown!", e);
          }
          try {
              if (!rte.target.equals(InetAddress.getLocalHost())) {
                  getRoute(rte);
                  if (rte.nifAddress == null)
                      findRoute(rte);
              } else {
                  logger.log(Level.WARNING, "localhost ignored!");
              }
          } catch (UnknownHostException e) {
              logger.log(Level.SEVERE, "localhost unknown!", e);
          }
          return rte;
      }
  
      class GetRouteTask implements Callable<Route> {
  
          String target;
  
          GetRouteTask(String target) {
              this.target = target;
          }
  
          public Route call() {
              return getOrFindRoute(target);
          }
      }
  
  
      private void findRoute(Route rte) {
          // First see whether we can even get to this system
          try {
              if (!rte.target.isReachable(TIMEOUT)) {
                  return;
              }
              ArrayList<Route> routeList = new ArrayList<Route>();
              for (NetworkInterface nif : ifList) {
  
                  // Try each interface...
                  if (!rte.target.isReachable(nif, 0, TIMEOUT))
                      continue;
  
                  Route rte2 = new Route();
                  rte2.host = rte.host;
                  rte2.target = rte.target;
                  rte2.nif = nif;
                  rte2.ttl = Short.MAX_VALUE;
                  rte2.reachable = true;
                  routeList.add(rte2);
              }
  
              if (routeList.size() == 0) {
                  logger.log(Level.SEVERE, "Target is reachable but not " +
                         "reachable through any listed route. Seems like a bug!");
                  return;
              }
  
  
              // If we can reach with multiple interfaces, find the best one
              selectLoop:
              while (routeList.size() > 1) {
                  int listSize = routeList.size();
                  for (int rteIdx = 0; rteIdx < listSize; rteIdx++) {
                      Route rte2 = routeList.get(rteIdx);
                      if (rte2.ttl == Integer.MAX_VALUE)
                          rte2.ttl = 63;
                      else if (rte2.ttl == 0)
                          break selectLoop;
                      if (!rte2.target.isReachable(rte.nif, rte.ttl, TIMEOUT)) {
                          routeList.remove(rteIdx);
                          continue selectLoop;
                      }
                      --rte2.ttl; // Keep decreasing...
                  }
              }
  
              Route rte2 = routeList.get(0); // Even if two of them make it to 0,
              // just choose the first.
  
              // We populate the value, but do not change the rte reference.
              rte.nif = rte2.nif;
              rte.ttl = rte2.ttl;
              rte.reachable = rte2.reachable;
  
              // We assume an interface has only one ip address per type.
              List<InterfaceAddress> ifAddresses =
                                              rte.nif.getInterfaceAddresses();
              for (InterfaceAddress ifAddress : ifAddresses)
                  if (ifAddress.getAddress().getAddress().length ==
                          rte.target.getAddress().length) {
                      rte.nifAddress = ifAddress.getAddress().getHostAddress();
                      break;
                  }
          } catch (IOException e) {
              logger.log(Level.WARNING, "Network error trying to reach " +
                          rte.host + '!', e);
          }
      }
  
      private void getRoute(Route route) {
  
          // Scan for a matching interface
          for (IFAddressInfo ifAInfo : ifAInfoList) {
              if (ifAInfo.netAddress.length != route.target.getAddress().length)
                  continue;
              byte[] targetNet;
              try {
                  targetNet = getNetworkAddress(route.target,
                                                ifAInfo.prefixLength);
              } catch (ConfigurationException e) {
                  StringBuilder buffer = new StringBuilder();
                  buffer.append(e.getMessage()).append('\n');
                  buffer = printIfInfo(ifAInfo, buffer);
                  logger.log(Level.WARNING, buffer.toString(), e);
                  continue;
              }
              if (Arrays.equals(ifAInfo.netAddress, targetNet)) {
                  route.nif = ifAInfo.nif;
                  route.nifAddress = ifAInfo.nifAddress.getAddress().
                                                              getHostAddress();
                  break;
              }
          }
  
          // Now try to ping it through the interface.
          try {
              route.reachable =
                      route.target.isReachable(route.nif, 1, LOCAL_TIMEOUT);
          } catch (IOException e) {
              logger.log(Level.WARNING, "Network error trying to reach " +
                          route.host + '!', e);
          }
          return;
      }
  
      void printIfInfo(IFAddressInfo ifAInfo) {
          if (logger.isLoggable(Level.FINE)) {
              logger.fine(printIfInfo(ifAInfo, new StringBuilder()).toString());
          }
      }
  
      StringBuilder printIfInfo(IFAddressInfo ifAInfo, StringBuilder buffer) {
          InetAddress ia = ifAInfo.nifAddress.getAddress();
          String ifName = ifAInfo.nif.getDisplayName();
          Formatter fmt = new Formatter(buffer);
          buffer.append(ifName);
          if (ia instanceof Inet4Address) {
              buffer.append("\tv4: " + ia.getHostAddress());
              buffer.append("\n\t\tNetwork " + ifAInfo.prefixLength + " bits\n");
              if (ifAInfo.netAddress != null) {
                  int[] netAddressI = new int[ifAInfo.netAddress.length];
                  for (int i = 0; i < netAddressI.length; i++) {
                      if (ifAInfo.netAddress[i] < 0) {
                          netAddressI[i] = 256 + ifAInfo.netAddress[i];
                      } else {
                          netAddressI[i] = ifAInfo.netAddress[i];
                      }
                  }
                  fmt.format("\t\tNet address: %d", netAddressI[0]);
                  for (int i = 1; i < ifAInfo.netAddress.length; i++) {
                      fmt.format(".%d", netAddressI[i]);
                  }
                  buffer.append('\n');
              } else {
                  buffer.append("\t\tNet address: null\n");
              }
          } else if (ia instanceof Inet6Address) {
              buffer.append("\tv6: " + ia.getHostAddress());
              buffer.append("\n\t\tNetwork " + ifAInfo.prefixLength + " bits\n");
              if (ifAInfo.netAddress != null) {
              int[] netAddressI = new int[ifAInfo.netAddress.length / 2];
              for (int i = 0; i < netAddressI.length; i++) {
                  int au = ifAInfo.netAddress[2 * i];
                  if (au < 0)
                      au += 256;
                  au <<= 8;
                  int au1 = ifAInfo.netAddress[2 * i + 1];
                  if (au1 < 0)
                      au1 += 256;
                  au |= au1;
                  netAddressI[i] = au;
              }
              fmt.format("\t\tNet address: %x", netAddressI[0]);
              for (int i = 1; i < netAddressI.length; i++)
                  fmt.format(":%x", netAddressI[i]);
              buffer.append('\n');
              } else {
                  buffer.append("\t\tNet address: null\n");
              }
          }
          return buffer;
      }
  
      private byte[] getNetworkAddress(InetAddress ia, short prefixLen)
              throws ConfigurationException {
          byte[] byteAddress = ia.getAddress();
          short networkBytes = (short) (prefixLen / 8);
          if (networkBytes > byteAddress.length) {
              throw new ConfigurationException(
              "Netmask too long. Network address " + ia + " has " +
                      prefixLen + "bit netmask while having " +
                      byteAddress.length + "bytes in the address. " +
                      "The address is a " + ia.getClass().getName());
          }
          byte[] netAddress = new byte[byteAddress.length];
          int bytePos = 0;
          for (; bytePos < networkBytes; bytePos++)
              netAddress[bytePos] = byteAddress[bytePos];
          short networkBits = (short) (prefixLen % 8);
          if (networkBits > 0) {
              byte mask = Byte.MIN_VALUE;
              for (int bitPos = 1; bitPos < networkBits; bitPos++)
                  mask |= mask >>> 1;
              netAddress[bytePos] = (byte) (byteAddress[bytePos] & mask);
          }
          return netAddress;
      }
  
      private void addInterfaces(Enumeration<NetworkInterface> interfaces,
                                       ArrayList<NetworkInterface> ifList)
              throws SocketException {
          while (interfaces.hasMoreElements()) {
              NetworkInterface nif = interfaces.nextElement();
              if (nif.isUp() && !nif.isLoopback()) {
                  ifList.add(nif);
                  addInterfaces(nif.getSubInterfaces(), ifList);
              }
          }
      }
  }
  