/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.netaddr;
import java.beans.*;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.text.*;
import java.util.*;
import net.java.sip.communicator.service.dns.*;
import net.java.sip.communicator.service.netaddr.*;
import net.java.sip.communicator.service.netaddr.event.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.Logger;
import net.java.sip.communicator.util.NetworkUtils;
import org.ice4j.*;
import org.ice4j.ice.*;
import org.ice4j.ice.harvest.*;
import org.ice4j.security.*;
import org.ice4j.stack.*;
import org.jitsi.service.configuration.*;
import org.jitsi.util.*;
/**
* This implementation of the Network Address Manager allows you to
* intelligently retrieve the address of your localhost according to the
* destinations that you will be trying to reach. It also provides an interface
* to the ICE implementation in ice4j.
*
* @author Emil Ivov
*/
public class NetworkAddressManagerServiceImpl
implements NetworkAddressManagerService
{
/**
* Our class logger.
*/
private static Logger logger =
Logger.getLogger(NetworkAddressManagerServiceImpl.class);
/**
* The socket that we use for dummy connections during selection of a local
* address that has to be used when communicating with a specific location.
*/
DatagramSocket localHostFinderSocket = null;
/**
* A random (unused)local port to use when trying to select a local host
* address to use when sending messages to a specific destination.
*/
private static final int RANDOM_ADDR_DISC_PORT = 55721;
/**
* The name of the property containing the number of binds that we should
* should execute in case a port is already bound to (each retry would be on
* a new random port).
*/
public static final String BIND_RETRIES_PROPERTY_NAME
= "net.java.sip.communicator.service.netaddr.BIND_RETRIES";
/**
* Default STUN server port.
*/
public static final int DEFAULT_STUN_SERVER_PORT = 3478;
/**
* A thread which periodically scans network interfaces and reports
* changes in network configuration.
*/
private NetworkConfigurationWatcher networkConfigurationWatcher = null;
/**
* The service name to use when discovering TURN servers through DNS using
* SRV requests as per RFC 5766.
*/
public static final String TURN_SRV_NAME = "turn";
/**
* The service name to use when discovering STUN servers through DNS using
* SRV requests as per RFC 5389.
*/
public static final String STUN_SRV_NAME = "stun";
/**
* Initializes this network address manager service implementation.
*/
public void start()
{
this.localHostFinderSocket = initRandomPortSocket();
// set packet logging to ice4j stack
StunStack.setPacketLogger(new Ice4jPacketLogger());
}
/**
* Kills all threads/processes launched by this thread (if any) and
* prepares it for shutdown. You may use this method as a reinitialization
* technique (you'll have to call start afterwards)
*/
public void stop()
{
try
{
if(networkConfigurationWatcher != null)
networkConfigurationWatcher.stop();
}
finally
{
logger.logExit();
}
}
/**
* Returns an InetAddress instance that represents the localhost, and that
* a socket can bind upon or distribute to peers as a contact address.
*
* @param intendedDestination the destination that we'd like to use the
* localhost address with.
*
* @return an InetAddress instance representing the local host, and that
* a socket can bind upon or distribute to peers as a contact address.
*/
public synchronized InetAddress getLocalHost(
InetAddress intendedDestination)
{
InetAddress localHost = null;
if(logger.isTraceEnabled())
{
logger.trace(
"Querying for a localhost address"
+ " for intended destination '"
+ intendedDestination
+ "'");
}
/* use native code (JNI) to find source address for a specific
* destination address on Windows XP SP1 and over.
*
* For other systems, we used method based on DatagramSocket.connect
* which will returns us source address. The reason why we cannot use it
* on Windows is because its socket implementation returns the any
* address...
*/
String osVersion;
if (OSUtils.IS_WINDOWS
&& !(osVersion = System.getProperty("os.version")).startsWith(
"4") /* 95/98/Me/NT */
&& !osVersion.startsWith("5.0")) /* 2000 */
{
byte[] src
= Win32LocalhostRetriever.getSourceForDestination(
intendedDestination.getAddress());
if (src == null)
{
logger.warn("Failed to get localhost ");
}
else
{
try
{
localHost = InetAddress.getByAddress(src);
}
catch(UnknownHostException uhe)
{
logger.warn("Failed to get localhost", uhe);
}
}
}
else if (OSUtils.IS_MAC)
{
try
{
localHost = BsdLocalhostRetriever
.getLocalSocketAddress(new InetSocketAddress(
intendedDestination, RANDOM_ADDR_DISC_PORT));
}
catch (IOException e)
{
logger.warn("Failed to get localhost", e);
}
}
else
{
//no point in making sure that the localHostFinderSocket is
//initialized.
//better let it through a NullPointerException.
localHostFinderSocket.connect(intendedDestination,
RANDOM_ADDR_DISC_PORT);
localHost = localHostFinderSocket.getLocalAddress();
localHostFinderSocket.disconnect();
}
//windows socket implementations return the any address so we need to
//find something else here ... InetAddress.getLocalHost seems to work
//better on windows so let's hope it'll do the trick.
if (localHost == null)
{
try
{
localHost = InetAddress.getLocalHost();
}
catch (UnknownHostException e)
{
logger.warn("Failed to get localhost ", e);
}
}
if (localHost.isAnyLocalAddress())
{
if (logger.isTraceEnabled())
{
logger.trace(
"Socket returned the ANY local address."
+ " Trying a workaround.");
}
try
{
//all that's inside the if is an ugly IPv6 hack
//(good ol' IPv6 - always causing more problems than it solves.)
if (intendedDestination instanceof Inet6Address)
{
//return the first globally routable ipv6 address we find
//on the machine (and hope it's a good one)
boolean done = false;
Enumeration<NetworkInterface> ifaces
= NetworkInterface.getNetworkInterfaces();
while (!done && ifaces.hasMoreElements())
{
Enumeration<InetAddress> addresses
= ifaces.nextElement().getInetAddresses();
while (addresses.hasMoreElements())
{
InetAddress address = addresses.nextElement();
if ((address instanceof Inet6Address)
&& !address.isAnyLocalAddress()
&& !address.isLinkLocalAddress()
&& !address.isLoopbackAddress()
&& !address.isSiteLocalAddress())
{
localHost = address;
done = true;
break;
}
}
}
}
else
// an IPv4 destination
{
// Make sure we got an IPv4 address.
if (intendedDestination instanceof Inet4Address)
{
// return the first non-loopback interface we find.
boolean done = false;
Enumeration<NetworkInterface> ifaces
= NetworkInterface.getNetworkInterfaces();
while (!done && ifaces.hasMoreElements())
{
Enumeration<InetAddress> addresses
= ifaces.nextElement().getInetAddresses();
while (addresses.hasMoreElements())
{
InetAddress address = addresses.nextElement();
if ((address instanceof Inet4Address)
&& !address.isLoopbackAddress())
{
localHost = address;
done = true;
break;
}
}
}
}
}
}
catch (Exception e)
{
//sigh ... ok return 0.0.0.0
logger.warn("Failed to get localhost", e);
}
}
if (logger.isTraceEnabled())
logger.trace("Returning the localhost address '" + localHost + "'");
return localHost;
}
/**
* Returns the hardware address (i.e. MAC address) of the specified
* interface name.
*
* @param iface the <tt>NetworkInterface</tt>
* @return array of bytes representing the layer 2 address or null if
* interface does not exist
*/
public byte[] getHardwareAddress(NetworkInterface iface)
{
String ifName = null;
byte hwAddress[] = null;
/* try reflection */
try
{
Method method = iface.getClass().
getMethod("getHardwareAddress");
if(method != null)
{
hwAddress = (byte[])method.invoke(iface, new Object[]{});
return hwAddress;
}
}
catch(Exception e)
{
}
/* maybe getHardwareAddress not available on this JVM try
* with our JNI
*/
if(OSUtils.IS_WINDOWS)
{
ifName = iface.getDisplayName();
}
else
{
ifName = iface.getName();
}
hwAddress = HardwareAddressRetriever.getHardwareAddress(ifName);
return hwAddress;
}
/**
* Tries to obtain an for the specified port.
*
* @param dst the destination that we'd like to use this address with.
* @param port the port whose mapping we are interested in.
* @return a public address corresponding to the specified port or null
* if all attempts to retrieve such an address have failed.
*
* @throws IOException if an error occurs while creating the socket.
* @throws BindException if the port is already in use.
*/
public InetSocketAddress getPublicAddressFor(InetAddress dst, int port)
throws IOException, BindException
{
//we'll try to bind so that we could notify the caller
//if the port has been taken already.
DatagramSocket bindTestSocket = new DatagramSocket(port);
bindTestSocket.close();
//if we're here then the port was free.
return new InetSocketAddress(getLocalHost(dst), port);
}
/**
* This method gets called when a bound property is changed.
* @param evt A PropertyChangeEvent object describing the event source
* and the property that has changed.
*/
public void propertyChange(PropertyChangeEvent evt)
{
//there's no point in implementing this method as we have no way of
//knowing whether the current property change event is the only event
//we're going to get or whether another one is going to follow..
//in the case of a STUN_SERVER_ADDRESS property change for example
//there's no way of knowing whether a STUN_SERVER_PORT property change
//will follow or not.
//Reinitializaion will therefore only happen if the reinitialize()
//method is called.
}
/**
* Initializes and binds a socket that on a random port number. The method
* would try to bind on a random port and retry 5 times until a free port
* is found.
*
* @return the socket that we have initialized on a randomport number.
*/
private DatagramSocket initRandomPortSocket()
{
DatagramSocket resultSocket = null;
String bindRetriesStr
= NetaddrActivator.getConfigurationService().getString(
BIND_RETRIES_PROPERTY_NAME);
int bindRetries = 5;
if (bindRetriesStr != null)
{
try
{
bindRetries = Integer.parseInt(bindRetriesStr);
}
catch (NumberFormatException ex)
{
logger.error(bindRetriesStr
+ " does not appear to be an integer. "
+ "Defaulting port bind retries to " + bindRetries
, ex);
}
}
int currentlyTriedPort = NetworkUtils.getRandomPortNumber();
//we'll first try to bind to a random port. if this fails we'll try
//again (bindRetries times in all) until we find a free local port.
for (int i = 0; i < bindRetries; i++)
{
try
{
resultSocket = new DatagramSocket(currentlyTriedPort);
//we succeeded - break so that we don't try to bind again
break;
}
catch (SocketException exc)
{
if (exc.getMessage().indexOf("Address already in use") == -1)
{
logger.fatal("An exception occurred while trying to create"
+ "a local host discovery socket.", exc);
return null;
}
//port seems to be taken. try another one.
if (logger.isDebugEnabled())
logger.debug("Port " + currentlyTriedPort
+ " seems in use.");
currentlyTriedPort
= NetworkUtils.getRandomPortNumber();
if (logger.isDebugEnabled())
logger.debug("Retrying bind on port "
+ currentlyTriedPort);
}
}
return resultSocket;
}
/**
* Creates a <tt>DatagramSocket</tt> and binds it to the specified
* <tt>localAddress</tt> and a port in the range specified by the
* <tt>minPort</tt> and <tt>maxPort</tt> parameters. We first try to bind
* the newly created socket on the <tt>preferredPort</tt> port number
* (unless it is outside the <tt>[minPort, maxPort]</tt> range in which case
* we first try the <tt>minPort</tt>) and then proceed incrementally upwards
* until we succeed or reach the bind retries limit. If we reach the
* <tt>maxPort</tt> port number before the bind retries limit, we will then
* start over again at <tt>minPort</tt> and keep going until we run out of
* retries.
*
* @param laddr the address that we'd like to bind the socket on.
* @param preferredPort the port number that we should try to bind to first.
* @param minPort the port number where we should first try to bind before
* moving to the next one (i.e. <tt>minPort + 1</tt>)
* @param maxPort the maximum port number where we should try binding
* before giving up and throwinG an exception.
*
* @return the newly created <tt>DatagramSocket</tt>.
*
* @throws IllegalArgumentException if either <tt>minPort</tt> or
* <tt>maxPort</tt> is not a valid port number or if <tt>minPort >
* maxPort</tt>.
* @throws IOException if an error occurs while the underlying resolver lib
* is using sockets.
* @throws BindException if we couldn't find a free port between
* <tt>minPort</tt> and <tt>maxPort</tt> before reaching the maximum allowed
* number of retries.
*/
public DatagramSocket createDatagramSocket(InetAddress laddr,
int preferredPort,
int minPort,
int maxPort)
throws IllegalArgumentException,
IOException,
BindException
{
// make sure port numbers are valid
if (!NetworkUtils.isValidPortNumber(minPort)
|| !NetworkUtils.isValidPortNumber(maxPort))
{
throw new IllegalArgumentException("minPort (" + minPort
+ ") and maxPort (" + maxPort + ") "
+ "should be integers between 1024 and 65535.");
}
// make sure minPort comes before maxPort.
if (minPort > maxPort)
{
throw new IllegalArgumentException("minPort (" + minPort
+ ") should be less than or "
+ "equal to maxPort (" + maxPort + ")");
}
// if preferredPort is not in the allowed range, place it at min.
if (minPort > preferredPort || preferredPort > maxPort)
{
throw new IllegalArgumentException("preferredPort ("+preferredPort
+") must be between minPort (" + minPort
+ ") and maxPort (" + maxPort + ")");
}
ConfigurationService config = NetaddrActivator
.getConfigurationService();
int bindRetries = config.getInt(BIND_RETRIES_PROPERTY_NAME,
BIND_RETRIES_DEFAULT_VALUE);
int port = preferredPort;
for (int i = 0; i < bindRetries; i++)
{
try
{
return new DatagramSocket(port, laddr);
}
catch (SocketException se)
{
if (logger.isInfoEnabled())
{
logger.info(
"Retrying a bind because of a failure to bind to address "
+ laddr + " and port " + port);
if (logger.isTraceEnabled())
logger.trace("Since you seem, here's a stack:", se);
}
}
port ++;
if (port > maxPort)
port = minPort;
}
throw new BindException("Could not bind to any port between "
+ minPort + " and " + (port -1));
}
/**
* Adds new <tt>NetworkConfigurationChangeListener</tt> which will
* be informed for network configuration changes.
*
* @param listener the listener.
*/
public synchronized void addNetworkConfigurationChangeListener(
NetworkConfigurationChangeListener listener)
{
if(networkConfigurationWatcher == null)
networkConfigurationWatcher = new NetworkConfigurationWatcher();
networkConfigurationWatcher
.addNetworkConfigurationChangeListener(listener);
}
/**
* Remove <tt>NetworkConfigurationChangeListener</tt>.
*
* @param listener the listener.
*/
public synchronized void removeNetworkConfigurationChangeListener(
NetworkConfigurationChangeListener listener)
{
if(networkConfigurationWatcher != null)
networkConfigurationWatcher
.removeNetworkConfigurationChangeListener(listener);
}
/**
* Creates and returns an ICE agent that a protocol could use for the
* negotiation of media transport addresses. One ICE agent should only be
* used for a single session negotiation.
*
* @return the newly created ICE Agent.
*/
public Agent createIceAgent()
{
return new Agent();
}
/**
* Tries to discover a TURN or a STUN server for the specified
* <tt>domainName</tt>. The method would first try to discover a TURN
* server and then fall back to STUN only. In both cases we would only care
* about a UDP transport.
*
* @param domainName the domain name that we are trying to discover a
* TURN server for.
* @param userName the name of the user we'd like to use when connecting to
* a TURN server (we won't be using credentials in case we only have a STUN
* server).
* @param password the password that we'd like to try when connecting to
* a TURN server (we won't be using credentials in case we only have a STUN
* server).
*
* @return A {@link StunCandidateHarvester} corresponding to the TURN or
* STUN server we discovered or <tt>null</tt> if there were no such records
* for the specified <tt>domainName</tt>
*/
public StunCandidateHarvester discoverStunServer(String domainName,
byte[] userName,
byte[] password)
{
String srvrAddress = null;
int port = 0;
try
{
SRVRecord srvRecord = NetworkUtils.getSRVRecord(
TURN_SRV_NAME, Transport.UDP.toString(), domainName);
if(srvRecord != null)
{
srvrAddress = srvRecord.getTarget();
}
if(srvrAddress != null)
{
//yay! we seem to have a TURN server, so we'll be using it for
//both TURN and STUN harvesting.
return new TurnCandidateHarvester(
new TransportAddress(srvrAddress,
srvRecord.getPort(),
Transport.UDP),
new LongTermCredential(userName, password));
}
//srvrAddres was null. try for a STUN only server.
srvRecord = NetworkUtils.getSRVRecord(
STUN_SRV_NAME, Transport.UDP.toString(), domainName);
if(srvRecord != null)
{
srvrAddress = srvRecord.getTarget();
port = srvRecord.getPort();
}
}
catch (ParseException e)
{
logger.info(domainName + " seems to be causing parse problems", e);
srvrAddress = null;
}
catch (DnssecException e)
{
logger.warn("DNSSEC validation for " + domainName
+ " STUN/TURN failed.", e);
}
if(srvrAddress != null)
{
return new StunCandidateHarvester(
new TransportAddress(
srvrAddress,
port,
Transport.UDP));
}
//srvrAddress was still null. sigh ...
return null;
}
/**
* Creates an <tt>IceMediaStrean</tt> and adds to it an RTP and and RTCP
* component, which also implies running the currently installed
* harvesters so that they would.
*
* @param rtpPort the port that we should try to bind the RTP component on
* (the RTCP one would automatically go to rtpPort + 1)
* @param streamName the name of the stream to create
* @param agent the <tt>Agent</tt> that should create the stream.
*
*@return the newly created <tt>IceMediaStream</tt>.
*
* @throws IllegalArgumentException if <tt>rtpPort</tt> is not a valid port
* number.
* @throws IOException if an error occurs while the underlying resolver
* is using sockets.
* @throws BindException if we couldn't find a free port between within the
* default number of retries.
*/
public IceMediaStream createIceStream( int rtpPort,
String streamName,
Agent agent)
throws IllegalArgumentException,
IOException,
BindException
{
return createIceStream(2, rtpPort, streamName, agent);
}
/**
* {@inheritDoc}
*/
public IceMediaStream createIceStream( int numComponents,
int portBase,
String streamName,
Agent agent)
throws IllegalArgumentException,
IOException,
BindException
{
if(numComponents < 1 || numComponents > 2)
throw new IllegalArgumentException(
"Invalid numComponents value: " + numComponents);
IceMediaStream stream = agent.createMediaStream(streamName);
agent.createComponent(
stream, Transport.UDP,
portBase, portBase, portBase + 100);
if(numComponents > 1)
{
agent.createComponent(
stream, Transport.UDP,
portBase + 1, portBase + 1, portBase + 101);
}
return stream;
}
}