/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) 2006 Aelitis, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * AELITIS, SAS au capital de 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */


package com.aelitis.azureus.core.networkmanager.admin.impl;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.*;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.UnsupportedAddressTypeException;
import java.util.*;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.platform.PlatformManager;
import org.gudy.azureus2.platform.PlatformManagerCapabilities;
import org.gudy.azureus2.platform.PlatformManagerFactory;
import org.gudy.azureus2.platform.PlatformManagerPingCallback;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.platform.PlatformManagerException;
import org.gudy.azureus2.plugins.utils.Utilities;
import org.gudy.azureus2.pluginsimpl.local.PluginInitializer;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.instancemanager.AZInstance;
import com.aelitis.azureus.core.instancemanager.AZInstanceManager;
import com.aelitis.azureus.core.instancemanager.AZInstanceManagerListener;
import com.aelitis.azureus.core.instancemanager.AZInstanceTracked;
import com.aelitis.azureus.core.networkmanager.admin.*;
import com.aelitis.azureus.core.networkmanager.impl.http.HTTPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.proxy.socks.AESocksProxy;
import com.aelitis.azureus.core.proxy.socks.AESocksProxyFactory;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;
import com.aelitis.azureus.plugins.upnp.UPnPPluginService;

public class 
NetworkAdminImpl
	extends NetworkAdmin
	implements AEDiagnosticsEvidenceGenerator
{
	private static final LogIDs LOGID = LogIDs.NWMAN;
	
	private static final boolean	FULL_INTF_PROBE	= false;
	
	private static InetAddress anyLocalAddress;
	private static InetAddress anyLocalAddressIPv4;
	private static InetAddress anyLocalAddressIPv6;
	private static InetAddress localhostV4;
	private static InetAddress localhostV6;
	
	static
	{
		try
		{
			anyLocalAddressIPv4 	= InetAddress.getByAddress(new byte[] { 0,0,0,0 });
			anyLocalAddressIPv6  	= InetAddress.getByAddress(new byte[] {0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0});
			anyLocalAddress			= new InetSocketAddress(0).getAddress();
			localhostV4 = InetAddress.getByAddress(new byte[] {127,0,0,1});
			localhostV6 = InetAddress.getByAddress(new byte[] {0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1});
		} catch (UnknownHostException e)
		{
			e.printStackTrace();
		}
	}

	
	
	
	private Set<NetworkInterface>		old_network_interfaces;
	private InetAddress[]				currentBindIPs			= new InetAddress[] { null };
	private boolean						supportsIPv6withNIO		= true;
	private boolean						supportsIPv6 = true;
	private boolean						supportsIPv4 = true;
	
	private boolean						IPv6_enabled;
	
	{
		COConfigurationManager.addAndFireParameterListener(
				"IPV6 Enable Support",
				new ParameterListener()
				{
					public void 
					parameterChanged(
						String parameterName )
					{
						setIPv6Enabled( COConfigurationManager.getBooleanParameter("IPV6 Enable Support"));
					}
				});
	}
	
	private int roundRobinCounterV4 = 0;
	private int roundRobinCounterV6 = 0;
	

	private CopyOnWriteList	listeners = new CopyOnWriteList();
	
	
	private NetworkAdminRouteListener
		trace_route_listener = new NetworkAdminRouteListener()
		{
			private int	node_count = 0;
			
			public boolean
			foundNode(
				NetworkAdminNode	node,
				int					distance,
				int					rtt )
			{
				node_count++;
				
				return( true );
			}
			
			public boolean
			timeout(
				int					distance )
			{
				if ( distance == 3 && node_count == 0 ){
					
					return( false );
				}
				
				return( true );
			}
		};
		
	private static final int ASN_MIN_CHECK = 30*60*1000;
	
	private long last_asn_lookup_time;
	
	private List asn_ips_checked = new ArrayList(0);
	
	private List as_history = new ArrayList();
	
	private AsyncDispatcher		async_asn_dispacher 	= new AsyncDispatcher();
	private static final int	MAX_ASYNC_ASN_LOOKUPS	= 1024;
	
	private Map<InetAddress, NetworkAdminASN>	async_asn_history = 
		new LinkedHashMap<InetAddress, NetworkAdminASN>(256,0.75f,true)
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry<InetAddress, NetworkAdminASN> eldest) 
			{
				return size() > 256;
			}
		};
		
	private boolean 	initialised;
	
	public
	NetworkAdminImpl()
	{
		COConfigurationManager.addParameterListener(
			new String[] {"Bind IP","Enforce Bind IP"},
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String parameterName )
				{
					checkDefaultBindAddress( false );
				}
			});
		
		SimpleTimer.addPeriodicEvent(
			"NetworkAdmin:checker",
			15000,
			new TimerEventPerformer()
			{
				public void 
				perform(
					TimerEvent event )
				{
					checkNetworkInterfaces( false, false );
				}
			});
		
			// populate initial values
		
		checkNetworkInterfaces( true, true );
		
		checkDefaultBindAddress( true );
		
		AEDiagnostics.addEvidenceGenerator( this );
		
		initialised = true;
	}
	
	protected void
	setIPv6Enabled(
		boolean enabled )
	{
		IPv6_enabled	= enabled;
		
		supportsIPv6withNIO		= enabled;
		supportsIPv6 			= enabled;

		if ( initialised ){
			
			checkNetworkInterfaces( false, true );
			
			checkDefaultBindAddress( false );
		}
	}
	
	protected void
	checkNetworkInterfaces(
		boolean		first_time,
		boolean		force )
	{
		try{
			Enumeration 	nis = NetworkInterface.getNetworkInterfaces();
		
			boolean	changed	= false;

			if ( nis == null && old_network_interfaces == null ){
				
			}else if ( nis == null ){
				
				old_network_interfaces	= null;
					
				changed = true;
					
			}else if ( old_network_interfaces == null ){
				
				Set	new_network_interfaces = new HashSet();
				
				while( nis.hasMoreElements()){

					new_network_interfaces.add( nis.nextElement());
				}
				
				old_network_interfaces = new_network_interfaces;
				
				changed = true;
				
			}else{
				
				Set	new_network_interfaces = new HashSet();
				
				while( nis.hasMoreElements()){
					
					Object	 ni = nis.nextElement();
					
						// NetworkInterface's "equals" method is based on ni name + addresses
					
					if ( !old_network_interfaces.contains( ni )){
						
						changed	= true;
					}
					
					new_network_interfaces.add( ni );
				}
					
				if ( old_network_interfaces.size() != new_network_interfaces.size()){
					
					changed = true;
				}
				
				old_network_interfaces = new_network_interfaces;
			}
			
			if ( changed || force ){
							
				boolean newV6 = false;
				boolean newV4 = false;
				
				Set<NetworkInterface> interfaces = old_network_interfaces;
				if (interfaces != null)
				{
					Iterator<NetworkInterface> it = interfaces.iterator();
					while (it.hasNext())
					{
						NetworkInterface ni = it.next();
						Enumeration addresses = ni.getInetAddresses();
						while (addresses.hasMoreElements())
						{
							InetAddress ia = (InetAddress) addresses.nextElement();

							if (ia.isLoopbackAddress()){
								continue;
							}
							if (ia instanceof Inet6Address && !ia.isLinkLocalAddress()){
								if ( IPv6_enabled ){
									newV6 = true;
								}
							}else if (ia instanceof Inet4Address){
								newV4 = true;
							}
						}
					}
				}
				
				supportsIPv4 = newV4;
				supportsIPv6 = newV6;
				
				Logger.log(new LogEvent(LOGID, "NetworkAdmin: ipv4 supported: "+supportsIPv4+"; ipv6: "+supportsIPv6+"; probing v6+nio functionality"));
				
				if(newV6)
				{
					ServerSocketChannel channel = ServerSocketChannel.open();
					
					try
					{
						channel.configureBlocking(false);
						channel.socket().bind(new InetSocketAddress(anyLocalAddressIPv6, 0));
						Logger.log(new LogEvent(LOGID, "NetworkAdmin: testing nio + ipv6 bind successful"));

						supportsIPv6withNIO = true;
					} catch (Exception e)
					{
						Logger.log(new LogEvent(LOGID,LogEvent.LT_WARNING, "nio + ipv6 test failed",e));
						supportsIPv6withNIO = false;
					}
					
					channel.close();
				} else
					supportsIPv6withNIO = false;
					
				if ( !first_time ){
					
					Logger.log(
						new LogEvent(LOGID,
								"NetworkAdmin: network interfaces have changed" ));
				}
				
				firePropertyChange( NetworkAdmin.PR_NETWORK_INTERFACES );
				
				checkDefaultBindAddress( first_time );
			}
		}catch( Throwable e ){
		}
	}
	
	public InetAddress getMultiHomedOutgoingRoundRobinBindAddress(InetAddress target)
	{
		InetAddress[]	addresses = currentBindIPs;
		boolean v6 = target instanceof Inet6Address; 
		int previous = (v6 ? roundRobinCounterV6 : roundRobinCounterV4) % addresses.length;
		InetAddress toReturn = null;
		
		int i = previous;

		do
		{
			i++;i%= addresses.length;
			if (target == null || (v6 && addresses[i] instanceof Inet6Address) || (!v6 && addresses[i] instanceof Inet4Address))
			{
				toReturn = addresses[i];
				break;
			} else if(!v6 && addresses[i].isAnyLocalAddress())
			{
				toReturn = anyLocalAddressIPv4;
				break;				
			}
		} while(i!=previous);
			
		if(v6)
			roundRobinCounterV6 = i;
		else
			roundRobinCounterV4 = i;
		return toReturn != null ? toReturn : (v6 ? localhostV6 : localhostV4);
	}
		
	public InetAddress[] getMultiHomedServiceBindAddresses(boolean nio)
	{
		InetAddress[] bindIPs = currentBindIPs;
		for(int i=0;i<bindIPs.length;i++)
		{
			if(bindIPs[i].isAnyLocalAddress())
				return new InetAddress[] {nio && !supportsIPv6withNIO && bindIPs[i] instanceof Inet6Address ? anyLocalAddressIPv4 : bindIPs[i]};
		}
		return bindIPs;
	}
	
	public InetAddress getSingleHomedServiceBindAddress(int proto)
	{
		InetAddress[] addrs = currentBindIPs;
		if(proto == IP_PROTOCOL_VERSION_AUTO){
			return addrs[0];
		}else{
			for( InetAddress addr: addrs ){
		
				if( (proto == IP_PROTOCOL_VERSION_REQUIRE_V4 && addr instanceof Inet4Address || addr.isAnyLocalAddress()) ||
					(proto == IP_PROTOCOL_VERSION_REQUIRE_V6 && addr instanceof Inet6Address) ){
				
					if ( addr.isAnyLocalAddress()){
						
						if ( proto == IP_PROTOCOL_VERSION_REQUIRE_V4 ){
							
							return( anyLocalAddressIPv4 );
							
						}else{
							
							return( anyLocalAddressIPv6 );
						}
					}else{
						
						return( addr );
					}
				}
			}
		}
		
		throw new UnsupportedAddressTypeException();
	}
	
	public InetAddress[]
	getAllBindAddresses(
		boolean	include_wildcard )
	{
		if ( include_wildcard ){
			
			return( currentBindIPs );
			
		}else{
			
			List<InetAddress> res = new ArrayList<InetAddress>();
			
			InetAddress[] bind_ips = currentBindIPs;
			
			for ( InetAddress ip: bind_ips ){
			
				if( !ip.isAnyLocalAddress()){
					
					res.add( ip );
				}
			}
			
			return( res.toArray( new InetAddress[ res.size()]));
		}
	}
	
	private InetAddress[] calcBindAddresses(final String addressString, boolean enforceBind)
	{
		ArrayList<InetAddress> addrs = new ArrayList<InetAddress>();
		
		Pattern addressSplitter = Pattern.compile(";");
		Pattern interfaceSplitter = Pattern.compile("[\\]\\[]");
		
		String[] tokens = addressSplitter.split(addressString);

addressLoop: 
		for(int i=0;i<tokens.length;i++)
		{
			String currentAddress = tokens[i];
			
			currentAddress = currentAddress.trim();
			
			if ( currentAddress.length() == 0 ){
				continue;
			}
			
			InetAddress parsedAddress = null;
			
			try
			{ // literal ipv4 or ipv6 address
				if(currentAddress.indexOf('.') != -1 || currentAddress.indexOf(':') != -1)
					parsedAddress = InetAddress.getByName(currentAddress);
			} catch (Exception e)
			{ // ignore, could be an interface name containing a ':'
			}
			
			if(parsedAddress != null)
			{
				try
				{
					// allow wildcard address as 1st address, otherwise only interface addresses
					if((!parsedAddress.isAnyLocalAddress() || addrs.size() > 0) && NetworkInterface.getByInetAddress(parsedAddress) == null)
						continue;
				} catch (SocketException e)
				{
					Debug.printStackTrace(e);
					continue;
				}
				addrs.add(parsedAddress);
				continue;
			}
				
			// interface name
			String[] ifaces = interfaceSplitter.split(currentAddress);

			NetworkInterface netInterface = null;
			try
			{
				netInterface = NetworkInterface.getByName(ifaces[0]);
			} catch (SocketException e)
			{
				e.printStackTrace(); // should not happen
			}
			if(netInterface == null)
				continue;

			Enumeration interfaceAddresses = netInterface.getInetAddresses();
			if(ifaces.length != 2)
				while(interfaceAddresses.hasMoreElements())
					addrs.add((InetAddress)interfaceAddresses.nextElement());
			else
			{
				int selectedAddress = 0;
				try { selectedAddress = Integer.parseInt(ifaces[1]); }
				catch (NumberFormatException e) {} // ignore, user could by typing atm
				for(int j=0;interfaceAddresses.hasMoreElements();j++,interfaceAddresses.nextElement())
					if(j==selectedAddress)
					{
						addrs.add((InetAddress)interfaceAddresses.nextElement());
						continue addressLoop;						
					}
			}
		}
		
		if ( !IPv6_enabled ){
			
			Iterator<InetAddress> it = addrs.iterator();
			
			while( it.hasNext()){
				
				if ( it.next() instanceof Inet6Address ){
					
					it.remove();
				}
			}
		}
		
		if(addrs.size() < 1){
			return new InetAddress[] {enforceBind ? localhostV4 : (hasIPV6Potential() ? anyLocalAddressIPv6 : anyLocalAddressIPv4)};
		}
		
		return( addrs.toArray(new InetAddress[addrs.size()]));

	}
	
	
	protected void checkDefaultBindAddress(boolean first_time)
	{
		boolean changed = false;
		String bind_ip = COConfigurationManager.getStringParameter("Bind IP", "").trim();
		boolean enforceBind = COConfigurationManager.getBooleanParameter("Enforce Bind IP");
		InetAddress[] addrs = calcBindAddresses(bind_ip, enforceBind);
		changed = !Arrays.equals(currentBindIPs, addrs);
		if(changed){
			currentBindIPs = addrs;
			if (!first_time)
			{
				String logmsg = "NetworkAdmin: default bind ip has changed to '";
				for(int i=0;i<addrs.length;i++)
					logmsg+=(addrs[i] == null ? "none" : addrs[i].getHostAddress()) + (i<addrs.length? ";" : "");
				logmsg+="'";
				Logger.log(new LogEvent(LOGID, logmsg));
			}
			firePropertyChange(NetworkAdmin.PR_DEFAULT_BIND_ADDRESS);
		}
	}
	
	public String getNetworkInterfacesAsString()
	{
		Set interfaces = old_network_interfaces;
		if (interfaces == null)
		{
			return ("");
		}
		Iterator it = interfaces.iterator();
		String str = "";
		while (it.hasNext())
		{
			NetworkInterface ni = (NetworkInterface) it.next();
			Enumeration addresses = ni.getInetAddresses();
			str+=ni.getName()+"\t("+ni.getDisplayName()+")\n";
			int i = 0;
			while(addresses.hasMoreElements()){
				InetAddress address = (InetAddress)addresses.nextElement();
				str+="\t"+ni.getName()+"["+(i++)+"]\t"+(address).getHostAddress()+"\n";
			}
		}
		return (str);
	}
	
	public boolean
	hasIPV4Potential()
	{
		return supportsIPv4;
	}
	
	public boolean
	hasIPV6Potential(boolean nio)
	{
		return nio ? supportsIPv6withNIO : supportsIPv6;
	}
	
	public InetAddress[]
  	getBindableAddresses()
  	{
  		List<InetAddress>	bindable = new ArrayList<InetAddress>();
  		
  		NetworkAdminNetworkInterface[] interfaces = NetworkAdmin.getSingleton().getInterfaces();
  		
  		for ( NetworkAdminNetworkInterface intf: interfaces ){
  			
  			NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();
  			
  			for ( NetworkAdminNetworkInterfaceAddress address: addresses ){

  				InetAddress a = address.getAddress();
  				
  				if ( canBind( a )){
  					
  					bindable.add( a );
  				}
  			}
  		}
  		
  		return( bindable.toArray( new InetAddress[ bindable.size()]));
  	}
  	
  	protected boolean
  	canBind(
  		InetAddress	bind_ip )
  	{
  		ServerSocketChannel ssc = null;
  		
  		try{
  			ssc = ServerSocketChannel.open();
  		
  			ssc.socket().bind( new InetSocketAddress(bind_ip,0), 16 );
  			
  			return( true );
  			
  		}catch( Throwable e ){
  			
  			return( false );
  			
  		}finally{
  			
  			if ( ssc != null ){
  	
  				try{
  					ssc.close();
  					
  				}catch( Throwable e ){
  					
  					Debug.out( e );
  				}
  			}
  		}
  	}
  	
	public int
	getBindablePort(
		int	prefer_port )
	
		throws IOException
	{
		final int tries = 1024;
		
		Random random = new Random();
		
		for ( int i=1;i<=tries;i++ ){
			
			int port;
			
			if ( i == 1 && prefer_port != 0 ){
				
				port = prefer_port;
				
			}else{
				
				port = i==tries?0:random.nextInt(20000) + 40000;
			}
			
			ServerSocketChannel ssc = null;
			
			try{
				ssc = ServerSocketChannel.open();

				ssc.socket().setReuseAddress( true );
				
				bind( ssc, null, port );

				port = ssc.socket().getLocalPort();
				
				ssc.close();
				
				return( port );
				
			}catch( Throwable e ){
				
				if ( ssc != null ){
					
					try{
						ssc.close();
						
					}catch( Throwable f ){
						
						Debug.printStackTrace(e);
					}
					
					ssc = null;
				}
			}
		}
		
		throw( new IOException( "No bindable ports found" ));
	}
	
	protected void
	bind(
		ServerSocketChannel	ssc,
		InetAddress			address,
		int					port )
	
		throws IOException
	{		
		if ( address == null ){
			
			ssc.socket().bind( new InetSocketAddress( port ), 1024 );
			
		}else{
			
			ssc.socket().bind( new InetSocketAddress( address, port ), 1024 );
		}
	}
	
	public InetAddress 
	guessRoutableBindAddress() 
	{
		try{
				// see if we have a choice
			
			List	local_addresses 	= new ArrayList();
			List	non_local_addresses = new ArrayList();
			
			try{
				NetworkAdminNetworkInterface[] interfaces = getInterfaces();
				
				List possible = new ArrayList();
				
				for (int i=0;i<interfaces.length;i++){
					
					NetworkAdminNetworkInterface intf = interfaces[i];
					
					NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();
					
					for (int j=0;j<addresses.length;j++){
						
						NetworkAdminNetworkInterfaceAddress address = addresses[j];
						
						InetAddress ia = address.getAddress();
						
						if ( ia.isLoopbackAddress()){
							
							continue;
						}
						
						if ( ia.isLinkLocalAddress() ||	ia.isSiteLocalAddress()){
							
							local_addresses.add( ia );
							
						}else{
							
							non_local_addresses.add( ia );
						}
						
						if ( 	( hasIPV4Potential() && ia instanceof Inet4Address ) ||
								( hasIPV6Potential() && ia instanceof Inet6Address )){
							
							possible.add( ia );
						}
					}
				}
				
				if ( possible.size() == 1 ){
					
					return((InetAddress)possible.get(0));
				}
			}catch( Throwable e ){				
			}
			
				// if we have a socks server then let's use a compatible address for it
			
			try{
				NetworkAdminSocksProxy[] socks = getSocksProxies();
				
				if ( socks.length > 0 ){
					
					return( mapAddressToBindIP( InetAddress.getByName( socks[0].getHost())));
				}
			}catch( Throwable e ){				
			}
			
				// next, same for nat devices
			
			try{
				NetworkAdminNATDevice[] nat = getNATDevices(AzureusCoreFactory.getSingleton());
				
				if ( nat.length > 0 ){
					
					return( mapAddressToBindIP( nat[0].getAddress()));
				}
			}catch( Throwable e ){				
			}
			
			try{
				final AESemaphore 	sem 		= new AESemaphore( "NA:conTest" );
				final InetAddress[]	can_connect = { null };
				
				final int	timeout = 10*1000;
				
				for (int i=0;i<local_addresses.size();i++){
					
					final InetAddress address = (InetAddress)local_addresses.get(i);
					
					new AEThread2( "NA:conTest", true )
					{
						public void
						run()
						{
							if ( canConnectWithBind( address, timeout )){
								
								can_connect[0] = address;
								
								sem.release();
							}
						}
					}.start();
				}
				
				if ( sem.reserve( timeout )){
					
					return( can_connect[0] );
				}
			}catch( Throwable e ){
				
			}
			
				// take a chance on any non local addresses we have
			
			if ( non_local_addresses.size() > 0 ){
				
				return( guessAddress( non_local_addresses ));
			}
			
				// lastly, select local one at random
			
			if ( local_addresses.size() > 0 ){
				
				return( guessAddress( local_addresses ));
			}
			
				// ho hum
			
			return( null );
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
			
			return( null );
		}
	}
	
	protected boolean
	canConnectWithBind(
		InetAddress	bind_address,
		int			timeout )
	{
		Socket	socket = null;
	
		try{
			socket = new Socket();

			socket.bind( new InetSocketAddress( bind_address, 0 ));

			socket.setSoTimeout( timeout );

			socket.connect( new InetSocketAddress( "www.google.com", 80 ), timeout );
			
			return( true );
			
		}catch( Throwable e ){
			
			return( false );
			
		}finally{
			
			if ( socket != null ){
				
				try{
					socket.close();
					
				}catch( Throwable f ){
				}
			}
		}
	}
	
	protected InetAddress
	mapAddressToBindIP(
		InetAddress	address )
	{
		boolean[]	address_bits = bytesToBits( address.getAddress());

		NetworkAdminNetworkInterface[] interfaces = getInterfaces();
		
		InetAddress	best_bind_address	= null;
		int			best_prefix			= 0;
		
		for (int i=0;i<interfaces.length;i++){
			
			NetworkAdminNetworkInterface intf = interfaces[i];
			
			NetworkAdminNetworkInterfaceAddress[] addresses = intf.getAddresses();
			
			for (int j=0;j<addresses.length;j++){
				
				NetworkAdminNetworkInterfaceAddress bind_address = addresses[j];
				
				InetAddress ba = bind_address.getAddress();

				byte[]	bind_bytes = ba.getAddress();
		
				if ( address_bits.length == bind_bytes.length ){
			
					boolean[]	bind_bits = bytesToBits( bind_bytes );

					for (int k=0;k<bind_bits.length;k++){
				
						if ( address_bits[k] != bind_bits[k] ){
					
							break;
						}
				
						if ( k > best_prefix ){
					
							best_prefix	= k;
					
							best_bind_address	= ba;
						}
					}
				}
			}
		}
		
		return( best_bind_address );
	}
		
	protected boolean[]
  	bytesToBits(
  		byte[]	bytes )
  	{
  		boolean[]	res = new boolean[bytes.length*8];
  		
  		for (int i=0;i<bytes.length;i++){
  			
  			byte	b = bytes[i];
  			
  			for (int j=0;j<8;j++){
  				
  				res[i*8+j] = (b&(byte)(0x01<<(7-j))) != 0;
  			}
  		}
  				
  		return( res );
  	}
	
	protected InetAddress
	guessAddress(
		List	addresses )
	{
			// prioritise 192.168.0.* and 192.168.1.* as common
			// then ipv4 over ipv6
		
		for (int i=0;i<addresses.size();i++){
			
			InetAddress address = (InetAddress)addresses.get(i);
			
			String str = address.getHostAddress();
			
			if ( str.startsWith( "192.168.0." ) || str.startsWith( "192.168.1." )){
				
				return( address );
			}
		}
		
		for (int i=0;i<addresses.size();i++){
			
			InetAddress address = (InetAddress)addresses.get(i);
			
			if ( address instanceof Inet4Address ){
				
				return( address );
			}
		}
		
		for (int i=0;i<addresses.size();i++){
			
			InetAddress address = (InetAddress)addresses.get(i);
			
			if ( address instanceof Inet6Address ){
				
				return( address );
			}
		}
		
		if ( addresses.size() > 0 ){
			
			return((InetAddress)addresses.get(0));
		}
		
		return( null );
	}
	
	public InetAddress
	getDefaultPublicAddress()
	{
		Utilities utils = PluginInitializer.getDefaultInterface().getUtilities();
		
		InetAddress address = utils.getPublicAddress();
		
		if ( address != null ){
			
			return( address );
		}
		
		return( utils.getPublicAddress( true ));
	}
	
	@Override
	public InetAddress getDefaultPublicAddressV6() {
		if(!supportsIPv6)
			return null;
		
		// check bindings first
		for(InetAddress addr : currentBindIPs)
		{
			// found a specific bind address, use that one
			if(AddressUtils.isGlobalAddressV6(addr))
				return addr;
			
			// found v6 any-local address, check interfaces for a best match
			if(addr instanceof Inet6Address && addr.isAnyLocalAddress())
			{
				ArrayList<InetAddress> addrs = new ArrayList<InetAddress>();
				for(NetworkInterface iface : old_network_interfaces)
					addrs.addAll(Collections.list(iface.getInetAddresses()));
				
				return AddressUtils.pickBestGlobalV6Address(addrs);
			}
		}
		
		return null;
	}
	
	public boolean
	hasDHTIPV6()
	{
		if ( hasIPV6Potential(false)){
			
			InetAddress v6 = getDefaultPublicAddressV6();
			
			return( v6 != null && !AddressUtils.isTeredo( v6 ));
		}
		
		return( false );
	}
	
	protected void
	firePropertyChange(
		String	property )
	{
		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((NetworkAdminPropertyChangeListener)it.next()).propertyChanged( property );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public NetworkAdminNetworkInterface[]
	getInterfaces()
	{
		Set	interfaces = old_network_interfaces;
		
		if ( interfaces == null ){
			
			return( new NetworkAdminNetworkInterface[0] );
		}
		
		NetworkAdminNetworkInterface[]	res = new NetworkAdminNetworkInterface[interfaces.size()];
		
		Iterator	it = interfaces.iterator();
				
		int	pos = 0;
		
		while( it.hasNext()){
			
			NetworkInterface ni = (NetworkInterface)it.next();

			res[pos++] = new networkInterface( ni );
		}
		
		return( res );
	}

	public NetworkAdminProtocol[]
 	getOutboundProtocols(
 			AzureusCore azureus_core)
	{
		NetworkAdminProtocol[]	res = 
			{
				new NetworkAdminProtocolImpl( azureus_core, NetworkAdminProtocol.PT_HTTP ),
				new NetworkAdminProtocolImpl( azureus_core, NetworkAdminProtocol.PT_TCP ),
				new NetworkAdminProtocolImpl( azureus_core, NetworkAdminProtocol.PT_UDP ),
			};
		      
		return( res );
	}
 	
 	public NetworkAdminProtocol[]
 	getInboundProtocols(
 			AzureusCore azureus_core)
 	{
		List	protocols = new ArrayList();
		
		TCPNetworkManager	tcp_manager = TCPNetworkManager.getSingleton();
		
		if ( tcp_manager.isTCPListenerEnabled()){
			
			protocols.add( 
					new NetworkAdminProtocolImpl( 
							azureus_core, 
							NetworkAdminProtocol.PT_TCP, 
							tcp_manager.getTCPListeningPortNumber()));
		}

		UDPNetworkManager	udp_manager = UDPNetworkManager.getSingleton();
		
		int	done_udp = -1;
		
		if ( udp_manager.isUDPListenerEnabled()){
			
			protocols.add( 
					new NetworkAdminProtocolImpl( 
							azureus_core, 
							NetworkAdminProtocol.PT_UDP, 
							done_udp = udp_manager.getUDPListeningPortNumber()));
		}
		
		if ( udp_manager.isUDPNonDataListenerEnabled()){

			int	port = udp_manager.getUDPNonDataListeningPortNumber();
			
			if ( port != done_udp ){
				
				protocols.add( 
						new NetworkAdminProtocolImpl( 
								azureus_core, 
								NetworkAdminProtocol.PT_UDP, 
								done_udp = udp_manager.getUDPNonDataListeningPortNumber()));
	
			}
		}
		
		HTTPNetworkManager	http_manager = HTTPNetworkManager.getSingleton();
		
		if ( http_manager.isHTTPListenerEnabled()){
			
			protocols.add( 
					new NetworkAdminProtocolImpl( 
							azureus_core, 
							NetworkAdminProtocol.PT_HTTP, 
							http_manager.getHTTPListeningPortNumber()));
		}
	      
		return((NetworkAdminProtocol[])protocols.toArray( new NetworkAdminProtocol[protocols.size()]));
 	}
 	
	public InetAddress
	testProtocol(
		NetworkAdminProtocol	protocol )
	
		throws NetworkAdminException
	{
		return( protocol.test( null ));
	}
	   
	public NetworkAdminSocksProxy[]
	getSocksProxies()
	{
		String host = System.getProperty( "socksProxyHost", "" ).trim();
		String port = System.getProperty( "socksProxyPort", "" ).trim();
        
		String user 		= System.getProperty("java.net.socks.username", "" ).trim();
		String password 	= System.getProperty("java.net.socks.password", "").trim();

		List	res = new ArrayList();
		
		NetworkAdminSocksProxyImpl	p1 = new NetworkAdminSocksProxyImpl( host, port, user, password );
		
		if ( p1.isConfigured()){
		
			res.add( p1 );
		}
		
		if ( 	COConfigurationManager.getBooleanParameter( "Proxy.Data.Enable" ) &&
				!COConfigurationManager.getBooleanParameter( "Proxy.Data.Same" )){
			
			host 	= COConfigurationManager.getStringParameter( "Proxy.Data.Host" );
			port	= COConfigurationManager.getStringParameter( "Proxy.Data.Port" );
			user	= COConfigurationManager.getStringParameter( "Proxy.Data.Username" );
			
			if ( user.trim().equalsIgnoreCase("<none>")){
				user = "";
			}
			password = COConfigurationManager.getStringParameter( "Proxy.Data.Password" );
			
			NetworkAdminSocksProxyImpl	p2 = new NetworkAdminSocksProxyImpl( host, port, user, password );
			
			if ( p2.isConfigured()){
			
				res.add( p2 );
			}			
		}

		return((NetworkAdminSocksProxy[])res.toArray(new NetworkAdminSocksProxy[res.size()]));
	}
	
	public NetworkAdminHTTPProxy
	getHTTPProxy()
	{
		NetworkAdminHTTPProxyImpl	res = new NetworkAdminHTTPProxyImpl();
		
		if ( !res.isConfigured()){
		
			res	= null;
		}
		
		return( res );
	}
	
	public NetworkAdminNATDevice[]
	getNATDevices(
			AzureusCore azureus_core )
	{
		List<NetworkAdminNATDeviceImpl>	devices = new ArrayList<NetworkAdminNATDeviceImpl>();
		
		try{
	
		    PluginInterface upnp_pi = azureus_core.getPluginManager().getPluginInterfaceByClass( UPnPPlugin.class );
		    
		    if ( upnp_pi != null ){
	    	
		    	UPnPPlugin upnp = (UPnPPlugin)upnp_pi.getPlugin();
		    	
		    	UPnPPluginService[]	services = upnp.getServices();
		    	
		    	for ( UPnPPluginService service: services ){
		    		
		    		NetworkAdminNATDeviceImpl dev = new NetworkAdminNATDeviceImpl( service );
		    		
		    		boolean same = false;
		    		
		    		for ( NetworkAdminNATDeviceImpl d: devices ){
		    			
		    			if ( d.sameAs( dev )){
		    				
		    				same = true;
		    				
		    				break;
		    			}
		    		}
		    		
		    		if ( !same ){
		    		
		    			devices.add( dev );
		    		}
		    	}
		    }
		}catch( Throwable e ){
			
			Debug.printStackTrace( e );
		}
		
		return((NetworkAdminNATDevice[])devices.toArray(new NetworkAdminNATDevice[devices.size()]));
	}
	
	public NetworkAdminASN 
	getCurrentASN() 
	{
		List	asns = COConfigurationManager.getListParameter( "ASN Details", new ArrayList());
		
		if ( asns.size() == 0 ){
	
				// migration from when we only persisted a single AS
			
			String as 	= "";
			String asn 	= "";
			String bgp 	= "";

			try{
				as 		= COConfigurationManager.getStringParameter( "ASN AS" );
				asn 	= COConfigurationManager.getStringParameter( "ASN ASN" );
				bgp 	= COConfigurationManager.getStringParameter( "ASN BGP" );
					
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
			
			COConfigurationManager.removeParameter( "ASN AS" );
			COConfigurationManager.removeParameter( "ASN ASN" );
			COConfigurationManager.removeParameter( "ASN BGP" );
			COConfigurationManager.removeParameter( "ASN Autocheck Performed Time" );
			
			asns.add(ASNToMap(new NetworkAdminASNImpl(as, asn, bgp )));
			
			COConfigurationManager.setParameter( "ASN Details", asns );
		}
		
		if ( asns.size() > 0 ){
			
			Map	m = (Map)asns.get(0);
			
			return( ASNFromMap( m ));
		}
		
		return( new NetworkAdminASNImpl( "", "", "" ));
	}
	
	protected Map
	ASNToMap(
		NetworkAdminASNImpl	x )
	{
		Map	m = new HashMap();
		
		byte[]	as	= new byte[0];
		byte[]	asn	= new byte[0];
		byte[]	bgp	= new byte[0];
		
		try{	
			as	= x.getAS().getBytes("UTF-8");
			asn	= x.getASName().getBytes("UTF-8");
			bgp	= x.getBGPPrefix().getBytes("UTF-8");
	
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
		
		m.put( "as", as );
		m.put( "name", asn );
		m.put( "bgp", bgp );
		
		return( m );
	}
	
	protected NetworkAdminASNImpl
	ASNFromMap(
		Map	m )
	{
		String	as		= "";
		String	asn		= "";
		String	bgp		= "";
		
		try{
			as	= new String((byte[])m.get("as"),"UTF-8");
			asn	= new String((byte[])m.get("name"),"UTF-8");
			bgp	= new String((byte[])m.get("bgp"),"UTF-8");
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
		
		return( new NetworkAdminASNImpl( as, asn, bgp ));
	}
	
	public NetworkAdminASN
	lookupCurrentASN(
		InetAddress		address )
	
		throws NetworkAdminException
	{
		NetworkAdminASN current = getCurrentASN();
		
		if ( current.matchesCIDR( address )){
			
			return( current );
		}
		
		List	asns = COConfigurationManager.getListParameter( "ASN Details", new ArrayList());

		for (int i=0;i<asns.size();i++){
			
			Map	m = (Map)asns.get(i);
			
			NetworkAdminASN x = ASNFromMap( m );
			
			if ( x.matchesCIDR( address )){
				
				asns.remove(i);
				
				asns.add( 0, m );
				
				firePropertyChange( PR_AS );
				
				return( x );
			}
		}
		
		if ( asn_ips_checked.contains( address )){
			
			return( current );
		}
				
		long now = SystemTime.getCurrentTime();

		if ( now < last_asn_lookup_time || now - last_asn_lookup_time > ASN_MIN_CHECK ){

			last_asn_lookup_time	= now;

			NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl( address );

			NetworkAdminASNImpl x = lookup.lookup();
			
			asn_ips_checked.add( address );
			
			asns.add( 0, ASNToMap( x ));
			
			firePropertyChange( PR_AS );

			return( x );
		}
		
		return( current );
	}
		
	public NetworkAdminASN
	lookupASN(
		InetAddress		address )
	
		throws NetworkAdminException
	{
		NetworkAdminASN existing = getFromASHistory( address );
		
		if ( existing != null ){
			
			return( existing );
		}

		NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl( address );

		NetworkAdminASNImpl result = lookup.lookup();
			
		addToASHistory( result );
		
		return( result );
	}
		
	public void
	lookupASN(
		final InetAddress					address,
		final NetworkAdminASNListener		listener )
	{
		synchronized( async_asn_history ){
			
			NetworkAdminASN existing = async_asn_history.get( address );
			
			if ( existing != null ){
				
				listener.success( existing );
			}
		}
		
		int	queue_size = async_asn_dispacher.getQueueSize();
				
		if ( queue_size >= MAX_ASYNC_ASN_LOOKUPS ){
			
			listener.failed( new NetworkAdminException( "Too many outstanding lookups" ));
			
		}else{
			
			async_asn_dispacher.dispatch(
				new AERunnable()
				{
					public void
					runSupport()
					{
						synchronized( async_asn_history ){
							
							NetworkAdminASN existing = async_asn_history.get( address );
							
							if ( existing != null ){
								
								listener.success( existing );
								
								return;
							}
						}
						
						try{				
							NetworkAdminASNLookupImpl lookup = new NetworkAdminASNLookupImpl( address );
					
							NetworkAdminASNImpl result = lookup.lookup();

							synchronized( async_asn_history ){

								async_asn_history.put( address, result );
							}
							
							listener.success( result );
							
						}catch( NetworkAdminException e ){
							
							listener.failed( e );
							
						}catch( Throwable e ){
							
							listener.failed( new NetworkAdminException( "lookup failed", e ));
						}
					}
				});
		}
	}
	
	protected void
	addToASHistory(
		NetworkAdminASN	asn )
	{
		synchronized( as_history ){
			
			boolean	found = false;
			
			for (int i=0;i<as_history.size();i++){
				
				 NetworkAdminASN x = (NetworkAdminASN)as_history.get(i);
				 
				 if ( asn.getAS() == x.getAS()){
					 
					 found = true;
					 
					 break;
				 }
			}
			
			if ( !found ){
				
				as_history.add( asn );
				
				if ( as_history.size() > 256 ){
					
					as_history.remove(0);
				}
			}
		}
	}
	
	protected NetworkAdminASN
	getFromASHistory(
		InetAddress	address )
	{
		synchronized( as_history ){
			
			for (int i=0;i<as_history.size();i++){
				
				 NetworkAdminASN x = (NetworkAdminASN)as_history.get(i);
				 
				 if ( x.matchesCIDR( address )){
					 
					 return( x );
				 }
			}
		}
		
		return( null );
	}
	
	public void
	runInitialChecks(
			AzureusCore azureus_core)
	{
		AZInstanceManager i_man = azureus_core.getInstanceManager();
		
		final AZInstance	my_instance = i_man.getMyInstance();
		
		i_man.addListener(
			new AZInstanceManagerListener()
			{
				private InetAddress external_address;
				
				public void
				instanceFound(
					AZInstance		instance )
				{
				}
				
				public void
				instanceChanged(
					AZInstance		instance )
				{
					if ( instance == my_instance ){
						
						InetAddress address = instance.getExternalAddress();
						
						if ( external_address == null || !external_address.equals( address )){
							
							external_address = address;
							
							try{
								lookupCurrentASN( address );
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
							}
						}
					}
				}
				
				public void
				instanceLost(
					AZInstance		instance )
				{
				}
				
				public void
				instanceTracked(
					AZInstanceTracked	instance )
				{
				}
			});
		
		if ( COConfigurationManager.getBooleanParameter( "Proxy.Check.On.Start" )){
			
			NetworkAdminSocksProxy[]	socks = getSocksProxies();
		
			for (int i=0;i<socks.length;i++){
				
				NetworkAdminSocksProxy	sock = socks[i];
				
				try{
					sock.getVersionsSupported();
			
				}catch( Throwable e ){
				
					Debug.printStackTrace( e );
					
					Logger.log(
						new LogAlert(
							true,
							LogAlert.AT_WARNING,
							"Socks proxy " + sock.getName() + " check failed: " + Debug.getNestedExceptionMessage( e )));
				}
			}
			
			NetworkAdminHTTPProxy http_proxy = getHTTPProxy();
			
			if ( http_proxy != null ){
			
				try{

					http_proxy.getDetails();
					
				}catch( Throwable e ){
					
					Debug.printStackTrace( e );
					
					Logger.log(
						new LogAlert(
							true,
							LogAlert.AT_WARNING,
							"HTTP proxy " + http_proxy.getName() + " check failed: " + Debug.getNestedExceptionMessage( e )));
				}
			}
		}

        NetworkAdminSpeedTestScheduler nast = NetworkAdminSpeedTestSchedulerImpl.getInstance();
        
        nast.initialise();
    }
	
	public boolean
	canTraceRoute()
	{
		PlatformManager	pm = PlatformManagerFactory.getPlatformManager();

		return( pm.hasCapability( PlatformManagerCapabilities.TraceRouteAvailability ));
	}
	
	public NetworkAdminNode[]
	getRoute(
		InetAddress						interface_address,
		InetAddress						target,
		final int						max_millis,
		final NetworkAdminRouteListener	listener )
	
		throws NetworkAdminException
	{
		PlatformManager	pm = PlatformManagerFactory.getPlatformManager();
			
		if ( !canTraceRoute()){
			
			throw( new NetworkAdminException( "No trace-route capability on platform" ));
		}
		
		final List	nodes = new ArrayList();
		
		try{
			pm.traceRoute( 
				interface_address,
				target,
				new PlatformManagerPingCallback()
				{
					private long	start_time = SystemTime.getCurrentTime();
					
					public boolean
					reportNode(
						int				distance,
						InetAddress		address,
						int				millis )
					{
						boolean	timeout	= false;
						
						if ( max_millis >= 0 ){
										
							long	now = SystemTime.getCurrentTime();
							
							if ( now < start_time ){
								
								start_time = now;
							}
							
							if ( now - start_time >= max_millis ){
								
								timeout = true;
							}
						}
						
						NetworkAdminNode	node = null;
						
						if ( address != null ){
							
							node = new networkNode( address, distance, millis );
							
							nodes.add( node );
						}
						
						boolean	result;
						
						if ( listener == null ){
							
							result = true;
							
						}else{

							if ( node == null ){
								
								result = listener.timeout( distance );
								
							}else{
								
								result =  listener.foundNode( node, distance, millis );
							}
						}
						
						return( result && !timeout );
					}
				});
		}catch( PlatformManagerException e ){
			
			throw( new NetworkAdminException( "trace-route failed", e ));
		}
		
		return((NetworkAdminNode[])nodes.toArray( new NetworkAdminNode[nodes.size()]));
	}
	
	public boolean
	canPing()
	{
		PlatformManager	pm = PlatformManagerFactory.getPlatformManager();

		return( pm.hasCapability( PlatformManagerCapabilities.PingAvailability ));
	}
	
	public NetworkAdminNode
	pingTarget(
		InetAddress						interface_address,
		InetAddress						target,
		final int						max_millis,
		final NetworkAdminRouteListener	listener )
	
		throws NetworkAdminException
	{
		PlatformManager	pm = PlatformManagerFactory.getPlatformManager();
			
		if ( !canPing()){
			
			throw( new NetworkAdminException( "No ping capability on platform" ));
		}
		
		final NetworkAdminNode[] nodes = { null };
		
		try{
			pm.ping(
				interface_address,
				target,
				new PlatformManagerPingCallback()
				{
					private long	start_time = SystemTime.getCurrentTime();
					
					public boolean
					reportNode(
						int				distance,
						InetAddress		address,
						int				millis )
					{
						boolean	timeout	= false;
						
						if ( max_millis >= 0 ){
										
							long	now = SystemTime.getCurrentTime();
							
							if ( now < start_time ){
								
								start_time = now;
							}
							
							if ( now - start_time >= max_millis ){
								
								timeout = true;
							}
						}
						
						NetworkAdminNode	node = null;
						
						if ( address != null ){
							
							node = new networkNode( address, distance, millis );
							
							nodes[0] = node;
						}
						
						boolean	result;
						
						if ( listener == null ){
							
							result = false;
							
						}else{

							if ( node == null ){
								
								result = listener.timeout( distance );
								
							}else{
								
								result =  listener.foundNode( node, distance, millis );
							}
						}
						
						return( result && !timeout );
					}
				});
		}catch( PlatformManagerException e ){
			
			throw( new NetworkAdminException( "ping failed", e ));
		}
		
		return( nodes[0] );
	}
	
	
	public void
	getRoutes(
		final InetAddress					target,
		final int							max_millis,
		final NetworkAdminRoutesListener	listener )
	
		throws NetworkAdminException
	{
		final List sems 	= new ArrayList();
		final List traces 	= new ArrayList();
		
		NetworkAdminNetworkInterface[] interfaces = getInterfaces();

		for (int i=0;i<interfaces.length;i++){
			
			NetworkAdminNetworkInterface	interf = (NetworkAdminNetworkInterface)interfaces[i];

			NetworkAdminNetworkInterfaceAddress[] addresses = interf.getAddresses();
			
			for (int j=0;j<addresses.length;j++){
				
				final NetworkAdminNetworkInterfaceAddress	address = addresses[j];
				
				InetAddress ia = address.getAddress();
				
				if ( ia.isLoopbackAddress() || ia instanceof Inet6Address ){
					
						// ignore
					
				}else{
					
					final AESemaphore sem = new AESemaphore( "parallelRouter" );
					
					final List		trace = new ArrayList();
					
					sems.add( sem );
					
					traces.add( trace );
					
					new AEThread2( "parallelRouter", true )
					{
						public void
						run()
						{
							try{
								address.getRoute( 
									target, 
									30000,
									new NetworkAdminRouteListener()
									{
										public boolean 
										foundNode(
											NetworkAdminNode 	node, 
											int 				distance, 
											int 				rtt ) 
										{
											trace.add( node );
											
											NetworkAdminNode[]	route = new NetworkAdminNode[trace.size()];
											
											trace.toArray( route );
											
											return( listener.foundNode( address, route, distance, rtt) );
										}
										
										public boolean 
										timeout(
											int distance )
										{
											NetworkAdminNode[]	route = new NetworkAdminNode[trace.size()];
											
											trace.toArray( route );

											return( listener.timeout( address, route, distance ));
										}
									});
								
							}catch( Throwable e ){
							
								e.printStackTrace();
								
							}finally{
								
								sem.release();
							}
						}
					}.start();
				}
			}
		}
		
		for (int i=0;i<sems.size();i++){
			
			((AESemaphore)sems.get(i)).reserve();
		}
	}
	
	public void
	pingTargets(
		final InetAddress					target,
		final int							max_millis,
		final NetworkAdminRoutesListener	listener )
	
		throws NetworkAdminException
	{
		final List sems 	= new ArrayList();
		final List traces 	= new ArrayList();
		
		NetworkAdminNetworkInterface[] interfaces = getInterfaces();

		for (int i=0;i<interfaces.length;i++){
			
			NetworkAdminNetworkInterface	interf = (NetworkAdminNetworkInterface)interfaces[i];

			NetworkAdminNetworkInterfaceAddress[] addresses = interf.getAddresses();
			
			for (int j=0;j<addresses.length;j++){
				
				final NetworkAdminNetworkInterfaceAddress	address = addresses[j];
				
				InetAddress ia = address.getAddress();
				
				if ( ia.isLoopbackAddress() || ia instanceof Inet6Address ){
					
						// ignore
					
				}else{
					
					final AESemaphore sem = new AESemaphore( "parallelPinger" );
					
					final List		trace = new ArrayList();
					
					sems.add( sem );
					
					traces.add( trace );
					
					new AEThread2( "parallelPinger", true )
					{
						public void
						run()
						{
							try{
								address.pingTarget( 
									target, 
									30000,
									new NetworkAdminRouteListener()
									{
										public boolean 
										foundNode(
											NetworkAdminNode 	node, 
											int 				distance, 
											int 				rtt ) 
										{
											trace.add( node );
											
											NetworkAdminNode[]	route = new NetworkAdminNode[trace.size()];
											
											trace.toArray( route );
											
											return( listener.foundNode( address, route, distance, rtt) );
										}
										
										public boolean 
										timeout(
											int distance )
										{
											NetworkAdminNode[]	route = new NetworkAdminNode[trace.size()];
											
											trace.toArray( route );

											return( listener.timeout( address, route, distance ));
										}
									});
								
							}catch( Throwable e ){
							
								e.printStackTrace();
								
							}finally{
								
								sem.release();
							}
						}
					}.start();
				}
			}
		}
		
		for (int i=0;i<sems.size();i++){
			
			((AESemaphore)sems.get(i)).reserve();
		}
	}
	
	public void
	addPropertyChangeListener(
		NetworkAdminPropertyChangeListener	listener )
	{
		listeners.add( listener );
	}
	
	public void
	addAndFirePropertyChangeListener(
		NetworkAdminPropertyChangeListener	listener )
	{
		listeners.add( listener );
		
		for (int i=0;i<NetworkAdmin.PR_NAMES.length;i++){
			
			try{
				listener.propertyChanged( PR_NAMES[i] );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public void
	removePropertyChangeListener(
		NetworkAdminPropertyChangeListener	listener )
	{
		listeners.remove( listener );
	}
	
	public void
	generate(
		IndentWriter		writer )
	{
		writer.println( "Network Admin" );
		
		try{
			writer.indent();
		
			try{
				writer.println( "Binding Details" );
				
				writer.indent();
				
				writer.println( "bind to: " + getString( getAllBindAddresses( false )));
				
				writer.println( "bindable: " + getString( getBindableAddresses()));
				
				writer.println( "ipv6_enabled=" + IPv6_enabled );

				writer.println( "ipv4_potential=" + hasIPV4Potential());
				writer.println( "ipv6_potential=" + hasIPV6Potential(false) + "/" + hasIPV6Potential(true));
	
				try{
					writer.println( "single homed: " + getSingleHomedServiceBindAddress());
				}catch( Throwable e ){
					writer.println( "single homed: none" );
				}
				
				try{
					writer.println( "single homed (4): " + getSingleHomedServiceBindAddress( IP_PROTOCOL_VERSION_REQUIRE_V4 ));
				}catch( Throwable e ){
					writer.println( "single homed (4): none" );
				}
				
				try{
					writer.println( "single homed (6): " + getSingleHomedServiceBindAddress( IP_PROTOCOL_VERSION_REQUIRE_V6 ));
				}catch( Throwable e ){
					writer.println( "single homed (6): none" );
				}
				
				writer.println( "multi homed, nio=false: " + getString( getMultiHomedServiceBindAddresses( false )));
				writer.println( "multi homed, nio=true:  " + getString( getMultiHomedServiceBindAddresses( true )));
					
			}finally{
				
				writer.exdent();
			}		
				
			NetworkAdminHTTPProxy	proxy = getHTTPProxy();
			
			if ( proxy == null ){
				
				writer.println( "HTTP proxy: none" );
				
			}else{
				
				writer.println( "HTTP proxy: " + proxy.getName());
				
				try{
					
					NetworkAdminHTTPProxy.Details details = proxy.getDetails();
					
					writer.println( "    name: " + details.getServerName());
					writer.println( "    resp: " + details.getResponse());
					writer.println( "    auth: " + details.getAuthenticationType());
					
				}catch( NetworkAdminException e ){
					
					writer.println( "    failed: " + e.getLocalizedMessage());
				}
			}
			
			NetworkAdminSocksProxy[]	socks = getSocksProxies();
			
			if ( socks.length == 0 ){
				
				writer.println( "Socks proxy: none" );
				
			}else{
				
				for (int i=0;i<socks.length;i++){
					
					NetworkAdminSocksProxy	sock = socks[i];
					
					writer.println( "Socks proxy: " + sock.getName());
					
					try{
						String[] versions = sock.getVersionsSupported();
						
						String	str = "";
						
						for (int j=0;j<versions.length;j++){
							
							str += (j==0?"":",") + versions[j];
						}
						
						writer.println( "   version: " + str );
						
					}catch( NetworkAdminException e ){
						
						writer.println( "    failed: " + e.getLocalizedMessage());
					}
				}
			}
			
			try {
				NetworkAdminNATDevice[]	nat_devices = getNATDevices(AzureusCoreFactory.getSingleton());

				writer.println( "NAT Devices: " + nat_devices.length );

				for (int i=0;i<nat_devices.length;i++){

					NetworkAdminNATDevice	device = nat_devices[i];

					writer.println( "    " + device.getName() + ",address=" + device.getAddress().getHostAddress() + ":" + device.getPort() + ",ext=" + device.getExternalAddress());
				}
			}catch (Exception e){
				
				writer.println( "Nat Devices: Can't get -> " + e.toString());
			}
  			
			writer.println( "Interfaces" );
			
			writer.println( "   " + getNetworkInterfacesAsString());
			
		}finally{
	
			writer.exdent();
		}
	}
	
	private String
	getString(
		InetAddress[]	addresses )
	{
		String	str = "";
		
		for ( InetAddress address: addresses ){
			
			str += (str.length()==0?"":", ") + address.getHostAddress();
		}
		
		return( str );
	}
	
	public void 
	generateDiagnostics(
		final IndentWriter iw )
	{
		Set	public_addresses = new HashSet();
		
		NetworkAdminHTTPProxy	proxy = getHTTPProxy();
		
		if ( proxy == null ){
			
			iw.println( "HTTP proxy: none" );
			
		}else{
			
			iw.println( "HTTP proxy: " + proxy.getName());
			
			try{
				
				NetworkAdminHTTPProxy.Details details = proxy.getDetails();
				
				iw.println( "    name: " + details.getServerName());
				iw.println( "    resp: " + details.getResponse());
				iw.println( "    auth: " + details.getAuthenticationType());
				
			}catch( NetworkAdminException e ){
				
				iw.println( "    failed: " + e.getLocalizedMessage());
			}
		}
		
		NetworkAdminSocksProxy[]	socks = getSocksProxies();
		
		if ( socks.length == 0 ){
			
			iw.println( "Socks proxy: none" );
			
		}else{
			
			for (int i=0;i<socks.length;i++){
				
				NetworkAdminSocksProxy	sock = socks[i];
				
				iw.println( "Socks proxy: " + sock.getName());
				
				try{
					String[] versions = sock.getVersionsSupported();
					
					String	str = "";
					
					for (int j=0;j<versions.length;j++){
						
						str += (j==0?"":",") + versions[j];
					}
					
					iw.println( "   version: " + str );
					
				}catch( NetworkAdminException e ){
					
					iw.println( "    failed: " + e.getLocalizedMessage());
				}
			}
		}
		
		try {
			NetworkAdminNATDevice[]	nat_devices = getNATDevices(AzureusCoreFactory.getSingleton());

			iw.println( "NAT Devices: " + nat_devices.length );

			for (int i=0;i<nat_devices.length;i++){

				NetworkAdminNATDevice	device = nat_devices[i];

				iw.println( "    " + device.getName() + ",address=" + device.getAddress().getHostAddress() + ":" + device.getPort() + ",ext=" + device.getExternalAddress());

				public_addresses.add( device.getExternalAddress());
			}
		} catch (Exception e) {
			iw.println( "Nat Devices: Can't get -> " + e.toString());
		}
 		
		iw.println( "Interfaces" );
		
		NetworkAdminNetworkInterface[] interfaces = getInterfaces();
		
		if ( FULL_INTF_PROBE ){
			
			if ( interfaces.length > 0 ){
				
				if ( interfaces.length > 1 || interfaces[0].getAddresses().length > 1 ){
					
					for (int i=0;i<interfaces.length;i++){
						
						networkInterface	interf = (networkInterface)interfaces[i];
						
						iw.indent();
						
						try{
							
							interf.generateDiagnostics( iw, public_addresses );
							
						}finally{
							
							iw.exdent();
						}
					}
				}else{
					
					if ( interfaces[0].getAddresses().length > 0 ){
						
						networkInterface.networkAddress address = (networkInterface.networkAddress)interfaces[0].getAddresses()[0];
						
						try{
							NetworkAdminNode[] nodes = address.getRoute( InetAddress.getByName("www.google.com"), 30000, trace_route_listener  );
							
							for (int i=0;i<nodes.length;i++){
								
								networkNode	node = (networkNode)nodes[i];
																
								iw.println( node.getString());
							}
						}catch( Throwable e ){
							
							iw.println( "Can't resolve host for route trace - " + e.getMessage());
						}
					}
				}
			}
		}else{
			
			try{
				pingTargets( 
					InetAddress.getByName( "www.google.com" ), 
					30000,
					new NetworkAdminRoutesListener()
					{
						private int	timeouts = 0;
						
						public boolean
						foundNode(
							NetworkAdminNetworkInterfaceAddress		intf,
							NetworkAdminNode[]						route,
							int										distance,
							int										rtt )
						{
							iw.println( intf.getAddress().getHostAddress() + ": " + route[route.length-1].getAddress().getHostAddress() + " (" + distance + ")" );

							return( false );
						}
						
						public boolean
						timeout(
							NetworkAdminNetworkInterfaceAddress		intf,
							NetworkAdminNode[]						route,
							int										distance )
						{
							iw.println( intf.getAddress().getHostAddress() + ": timeout (dist=" + distance + ")" );
							
							timeouts++;
							
							return( timeouts < 3 );
						}
					});
				
			}catch( Throwable e ){
				
				iw.println( "getRoutes failed: " + Debug.getNestedExceptionMessage( e ));
			}
		}
		
		iw.println( "Inbound protocols: default routing" );
		

		if (AzureusCoreFactory.isCoreRunning()) {
			AzureusCore azureus_core = AzureusCoreFactory.getSingleton();

			NetworkAdminProtocol[]	protocols = getInboundProtocols(azureus_core);
		
  		for (int i=0;i<protocols.length;i++){
  			
  			NetworkAdminProtocol	protocol = protocols[i];
  			
  			try{
  				InetAddress	ext_addr = testProtocol( protocol );
  	
  				if ( ext_addr != null ){
  					
  					public_addresses.add( ext_addr );
  				}
  	
  				iw.println( "    " + protocol.getName() + " - " + ext_addr );
  				
  			}catch( NetworkAdminException e ){
  				
  				iw.println( "    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
  			}
  		}
  		
  		iw.println( "Outbound protocols: default routing" );
  		
  		protocols = getOutboundProtocols(azureus_core);
  		
  		for (int i=0;i<protocols.length;i++){
  			
  			NetworkAdminProtocol	protocol = protocols[i];
  			
  			try{
  
  				InetAddress	ext_addr = testProtocol( protocol );
  				
  				if ( ext_addr != null ){
  				
  					public_addresses.add( ext_addr );
  				}
  				
  				iw.println( "    " + protocol.getName() + " - " + ext_addr );
  				
  			}catch( NetworkAdminException e ){
  				
  				iw.println( "    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
  			}
  		}
		}
		
		Iterator	it = public_addresses.iterator();
		
		iw.println( "Public Addresses" );
		
		while( it.hasNext()){
			
			InetAddress	pub_address = (InetAddress)it.next();
			
			try{
				NetworkAdminASN	res = lookupCurrentASN( pub_address );
				
				iw.println( "    " + pub_address.getHostAddress() + " -> " + res.getAS() + "/" + res.getASName());
				
			}catch( Throwable e ){
				
				iw.println( "    " + pub_address.getHostAddress() + " -> " + Debug.getNestedExceptionMessage(e));
			}
		}
	}
	
	protected class
	networkInterface
		implements NetworkAdminNetworkInterface
	{
		private NetworkInterface		ni;
		
		protected
		networkInterface(
			NetworkInterface	_ni )
		{
			ni	= _ni;
		}
		
		public String
		getDisplayName()
		{
			return( ni.getDisplayName());
		}
		
		public String
		getName()
		{
			return( ni.getName());
		}
		
		public NetworkAdminNetworkInterfaceAddress[]
		getAddresses()
		{
				// BAH NetworkInterface has lots of goodies but is 1.6
			
			Enumeration	e = ni.getInetAddresses();
		
			List	addresses = new ArrayList();
			
			while( e.hasMoreElements()){
				
				InetAddress address = (InetAddress)e.nextElement();
				
				if ((address instanceof Inet6Address) && !IPv6_enabled ){
					
					continue;
				}
				
				addresses.add( new networkAddress(address));
			}
	
			return((NetworkAdminNetworkInterfaceAddress[])addresses.toArray( new NetworkAdminNetworkInterfaceAddress[addresses.size()]));
		}
	
		public String
		getString()
		{
			String	str = getDisplayName() + "/" + getName() + " [";
			
			NetworkAdminNetworkInterfaceAddress[] addresses = getAddresses();
			
			for (int i=0;i<addresses.length;i++){
				
				networkAddress	addr = (networkAddress)addresses[i];
				
				str += (i==0?"":",") + addr.getAddress().getHostAddress();
			}
			
			return( str + "]" );
		}
		
		public void 
		generateDiagnostics(
			IndentWriter 	iw,
			Set				public_addresses )
		{
			iw.println( getDisplayName() + "/" + getName());
			
			NetworkAdminNetworkInterfaceAddress[] addresses = getAddresses();
			
			for (int i=0;i<addresses.length;i++){
				
				networkAddress	addr = (networkAddress)addresses[i];
				
				iw.indent();
				
				try{
					
					addr.generateDiagnostics( iw, public_addresses );
					
				}finally{
					
					iw.exdent();
				}
			}
		}
		

		protected class
		networkAddress
			implements NetworkAdminNetworkInterfaceAddress
		{
			private InetAddress		address;
			
			protected
			networkAddress(
					
				InetAddress	_address )
			{
				address = _address;
			}
			
			public NetworkAdminNetworkInterface
			getInterface()
			{
				return( networkInterface.this );
			}
			
			public InetAddress
			getAddress()
			{
				return( address );
			}
			
			public boolean
			isLoopback()
			{
				return( address.isLoopbackAddress());
			}
						
			public NetworkAdminNode[]
			getRoute(
				InetAddress						target,
				final int						max_millis,
				final NetworkAdminRouteListener	listener )
			
				throws NetworkAdminException
			{
				return( NetworkAdminImpl.this.getRoute( address, target, max_millis, listener));
			}
			
			public NetworkAdminNode
			pingTarget(
				InetAddress						target,
				final int						max_millis,
				final NetworkAdminRouteListener	listener )
			
				throws NetworkAdminException
			{
				return( NetworkAdminImpl.this.pingTarget( address, target, max_millis, listener));
			}
			
			public InetAddress
			testProtocol(
				NetworkAdminProtocol	protocol )
			
				throws NetworkAdminException
			{
				return( protocol.test( this ));
			}
			
			public void 
			generateDiagnostics(
				IndentWriter 	iw,
				Set				public_addresses )
			{
				iw.println( "" + getAddress());
				
				try{
					iw.println( "  Trace route" );
					
					iw.indent();
					
					if ( isLoopback()){
						
						iw.println( "Loopback - ignoring" );
						
					}else{
						
						try{
							NetworkAdminNode[] nodes = getRoute( InetAddress.getByName("www.google.com"), 30000, trace_route_listener );
							
							for (int i=0;i<nodes.length;i++){
								
								networkNode	node = (networkNode)nodes[i];
																
								iw.println( node.getString());
							}
						}catch( Throwable e ){
							
							iw.println( "Can't resolve host for route trace - " + e.getMessage());
						}
												
						iw.println( "Outbound protocols: bound" );
						
						AzureusCore azureus_core = AzureusCoreFactory.getSingleton();
						
						NetworkAdminProtocol[]	protocols = getOutboundProtocols(azureus_core);
						
						for (int i=0;i<protocols.length;i++){
							
							NetworkAdminProtocol	protocol = protocols[i];
							
							try{
								InetAddress	res = testProtocol( protocol );
								
								if ( res != null ){
									
									public_addresses.add( res );
								}
								
								iw.println( "    " + protocol.getName() + " - " + res );
								
							}catch( NetworkAdminException e ){
								
								iw.println( "    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
							}
						}
						
						iw.println( "Inbound protocols: bound" );
						
						protocols = getInboundProtocols(azureus_core);
						
						for (int i=0;i<protocols.length;i++){
							
							NetworkAdminProtocol	protocol = protocols[i];
							
							try{
								InetAddress	res = testProtocol( protocol );
								
								if ( res != null ){
									
									public_addresses.add( res );
								}
								
								iw.println( "    " + protocol.getName() + " - " + res );
								
							}catch( NetworkAdminException e ){
								
								iw.println( "    " + protocol.getName() + " - " + Debug.getNestedExceptionMessage(e));
							}
						}
					}
				}finally{
					
					iw.exdent();
				}
			}
		}
	}
	
	protected class
	networkNode
		implements NetworkAdminNode
	{
		private InetAddress	address;
		private int			distance;
		private int			rtt;
		
		protected
		networkNode(
			InetAddress		_address,
			int				_distance,
			int				_millis )
		{
			address		= _address;
			distance	= _distance;
			rtt			= _millis;
		}
		
		public InetAddress
		getAddress()
		{
			return( address );
		}
		
		public boolean
		isLocalAddress()
		{
			return( address.isLinkLocalAddress() ||	address.isSiteLocalAddress()); 
		}

		public int
		getDistance()
		{
			return( distance );
		}
		
		public int
		getRTT()
		{
			return( rtt );
		}
		
		protected String
		getString()
		{
			if ( address == null ){
				
				return( "" + distance );
				
			}else{
			
				return( distance + "," + address + "[local=" + isLocalAddress() + "]," + rtt );
			}
		}
	}
	
	protected void
	generateDiagnostics(
		IndentWriter			iw,
		NetworkAdminProtocol[]	protocols )
	{
		for (int i=0;i<protocols.length;i++){
			
			NetworkAdminProtocol	protocol = protocols[i];
			
			iw.println( "Testing " + protocol.getName());
			
			try{
				InetAddress	ext_addr = testProtocol( protocol );
	
				iw.println( "    -> OK, public address=" + ext_addr );
				
			}catch( NetworkAdminException e ){
				
				iw.println( "    -> Failed: " + Debug.getNestedExceptionMessage(e));
			}
		}
	}
	
	public void
	logNATStatus(
		IndentWriter		iw )
	{
		if (AzureusCoreFactory.isCoreRunning()) {
			generateDiagnostics( iw, getInboundProtocols(AzureusCoreFactory.getSingleton()));
		}
	}
	
	public static void
	main(
		String[]	args )
	{
		boolean	TEST_SOCKS_PROXY 	= false;
		boolean	TEST_HTTP_PROXY		= false;
		
		try{
			if ( TEST_SOCKS_PROXY ){
				
				AESocksProxy proxy = AESocksProxyFactory.create( 4567, 10000, 10000 );
				
				proxy.setAllowExternalConnections( true );
				
				System.setProperty( "socksProxyHost", "localhost" );
				System.setProperty( "socksProxyPort", "4567" );
			}
			
			if ( TEST_HTTP_PROXY ){
			   
				System.setProperty("http.proxyHost", "localhost" );
			    System.setProperty("http.proxyPort", "3128" );
			    System.setProperty("https.proxyHost", "localhost" );
			    System.setProperty("https.proxyPort", "3128" );
			    			    
				Authenticator.setDefault(
						new Authenticator()
						{
							protected PasswordAuthentication
							getPasswordAuthentication()
							{
								return( new PasswordAuthentication( "fred", "bill".toCharArray()));
							}
						});

			}
			
			IndentWriter iw = new IndentWriter( new PrintWriter( System.out ));
			
			iw.setForce( true );
			
			COConfigurationManager.initialise();
			
			AzureusCoreFactory.create();
			
			NetworkAdmin admin = getSingleton();
			
			//admin.logNATStatus( iw );
			admin.generateDiagnostics( iw );
			
		}catch( Throwable e){
			
			e.printStackTrace();
		}
	}
}
