/*
* Copyrigth (C) 2010 Henrik Baastrup.
*
* Licensed under the GNU Lesser General Public License version 3;
* you may not use this file except in compliance with the License.
* You should have received a copy of the license together with this
* file but can obtain a copy of the License at:
*
* http://www.gnu.org/licenses/lgpl-3.0.txt
*
* 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 javax.net.stun;
import javax.net.stun.dns.DClass;
import javax.net.stun.dns.DMessage;
import javax.net.stun.dns.DNSResolver;
import javax.net.stun.dns.DResource;
import javax.net.stun.dns.DResourceComparator;
import javax.net.stun.dns.DType;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* This class implement a STUN client as descriped in RFC 3489.
*
* @author Henrik Baastrup
*/
public class StunClient {
/**
* Values to discripe the test passed under the {@link StunClient#binding} method
*/
public enum DoneBindingTest {
NO_TEST,
TEST1_FIRST_RUN,
TEST2,
TEST1_SECOND_RUN,
TEST3
}
private String serverAddress = "";
private int serverPort = 3478;
private InetAddress localAddr = null;
private int localPort = 0;
public MessageAttribute mappedAddress = null;
private MessageAttribute changedAddress = null;
private DoneBindingTest bindingTestDone = DoneBindingTest.NO_TEST;
protected boolean debug = false;
protected static boolean staticDebug = false;
/**
* Default Creator
*
*/
public StunClient() {
}
/**
* Creator
* @param address for the STUN server (dafult prot used is 3478).
*/
public StunClient(String address) {
this.serverAddress = address;
}
/**
* Creator
* @param address for the STUN srever.
* @param port used to connect to the STUN server.
*/
public StunClient(String address, int port) {
this.serverAddress = address;
this.serverPort = port;
}
/**
*
* @param domainAddress ex
* @param dnsServer
* @return an array of StunServerAddress in priotated order
* @throws IOException
*/
public static StunServerAddress[] discovery(String domainAddress, String dnsServer) {
DNSResolver resolver = new DNSResolver(dnsServer);
String query = "_stun._udp."+domainAddress;
DResource answers[];
try {
DMessage message = resolver.query(query, DType.SRV, DClass.IN);
if (message==null) return new StunServerAddress[0];
answers = message.getAnswers();
if (answers==null || answers.length==0) {
message = resolver.query(domainAddress, DType.A, DClass.IN);
answers = message.getAnswers();
if (answers==null || answers.length==0) return new StunServerAddress[0];
}
} catch (IOException ex) {
if (staticDebug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
return new StunServerAddress[0];
}
DResourceComparator comparator = new DResourceComparator();
Arrays.sort(answers, comparator);
StunServerAddress retAddresses[] = new StunServerAddress[answers.length];
for (int i=0; i<answers.length; i++) {
StunServerAddress sAddr = new StunServerAddress();
if (answers[i].getDType()==DType.SRV) {
sAddr.address = answers[i].getTarget();
sAddr.port = answers[i].getPort();
}
else if (answers[i].getDType()==DType.A) {
sAddr.address = answers[i].getIpAddress();
sAddr.port = 3478;
}
retAddresses[i] = sAddr;
}
return retAddresses;
}
/**
* Ask the STUN server for a sgared secret if pocible.
* @return {@link SharedSecret} if the process suceed else null.
* @throws IOException
*/
public SharedSecret getSharedSecret() {
SSLSocket sslSocket = null;
MessageHeader header = null;
SharedSecret sharedSecret = null;
if (System.getProperty("javax.net.ssl.trustStore")==null)
System.setProperty("javax.net.ssl.trustStore", "StunTest.jks");
if (System.getProperty("javax.net.ssl.keyStoreType")==null)
System.setProperty("javax.net.ssl.keyStoreType", "JKS");
//System.setProperty("javax.net.debug", "help");
//System.setProperty("javax.net.debug", "ssl");
//System.setProperty("javax.net.debug", "ssl:record");
//System.setProperty("javax.net.debug", "ssl:handshake");
try {
//sock = new Socket(serverAddress, serverPort);
//sock.setSoTimeout(9500);
SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
sslSocket = (SSLSocket)sslFactory.createSocket(serverAddress, serverPort);
//sslSocket = (SSLSocket)sslFactory.createSocket(sock, serverAddress, serverPort, true);
sslSocket.startHandshake();
//Read the header
header = new MessageHeader(MessageHeader.HeaderType.SHARED_SECRET_REQUEST);
header.genrateTransactionId();
header = Utils.socketSendRecive(sslSocket, header);
} catch (IOException ex) {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
sharedSecret = new SharedSecret(700, ex.getMessage());
return sharedSecret;
} finally {
if (sslSocket!=null) try{sslSocket.close();} catch (IOException ignore){}
}
MessageAttribute errorCode = header.getMessageAttribute(MessageAttribute.MessageAttributeType.ERROR_CODE);
MessageAttribute usernameAttr = header.getMessageAttribute(MessageAttribute.MessageAttributeType.USERNAME);
MessageAttribute passwordAttr = header.getMessageAttribute(MessageAttribute.MessageAttributeType.PASSWORD);
if (errorCode!=null) {
sharedSecret = new SharedSecret(errorCode);
}
if (usernameAttr==null || passwordAttr==null) {
sharedSecret = new SharedSecret(700, "The server is sending an incomplete response (Username and Password message attributes are missing). The client should not retry.");
}
else {
sharedSecret = new SharedSecret(usernameAttr.getUsername(), passwordAttr.getPassword());
}
return sharedSecret;
}
/**
* Set the STUN server address property.
*
* @param arg0 server address
*/
public void setServerAddress(String arg0) {serverAddress = arg0;}
/**
* Set the STUN server port property.
*
* @param arg0 port (default value = 3478).
*/
public void setServerPort(int arg0) {serverPort = arg0;}
/**
* Do the binding process as descriped in RFC 3489:
* <pre> {@code
+--------+
| Test |
| I |
+--------+
|
|
V
/\ /\
N / \ Y / \ Y +--------+
UDP <-------/Resp\--------->/ IP \------------->| Test |
Blocked \ ? / \Same/ | II |
\ / \? / +--------+
\/ \/ |
| N |
| V
V /\
+--------+ Sym. N / \
| Test | UDP <---/Resp\
| II | Firewall \ ? /
+--------+ \ /
| \/
V |Y
/\ /\ |
Symmetric N / \ +--------+ N / \ V
NAT <--- / IP \<-----| Test |<--- /Resp\ Open
\Same/ | I | \ ? / Internet
\? / +--------+ \ /
\/ \/
| |Y
| |
| V
| Full
| Cone
V /\
+--------+ / \ Y
| Test |------>/Resp\---->Restricted
| III | \ ? /
+--------+ \ /
\/
|N
| Port
+------>Restricted
* }</pre>
*
* @param sharedSecret found in the @{link getSharedSecret} method or null if no secret was found or used.
* @return @{link DiscoveryInfo} contining the informations from the STUN server found by the process
*/
public DiscoveryInfo binding(SharedSecret sharedSecret) {
DiscoveryInfo discoveryInfo = new DiscoveryInfo();
if (test1(discoveryInfo, sharedSecret, true)) {
if (test2(discoveryInfo, sharedSecret)) {
if (test1(discoveryInfo, sharedSecret, false)) {
test3(discoveryInfo, sharedSecret);
}
}
}
return discoveryInfo;
}
/**
* This method will only do the first test in the binding process, and is
* usefull if one only is intrested in finding the remote address of the
* client, and not the senario.
*
* @param sharedSecret @{link SharedSecret} found in @{link getSharedSecret} method or null.
* @return DiscoveryInfo that contain the remote address value.
*/
public DiscoveryInfo bindForRemoteAddressOnly(SharedSecret sharedSecret) {
DiscoveryInfo discoveryInfo = new DiscoveryInfo();
test1(discoveryInfo, sharedSecret, true);
return discoveryInfo;
}
private boolean test1(DiscoveryInfo discoveryInfo, SharedSecret sharedSecret, boolean firstRun) {
MessageHeader header = new MessageHeader(MessageHeader.HeaderType.BINDING_REQUEST);
header.genrateTransactionId();
//byte hmac[] = setMessageAttributes(header, (byte)0, sharedSecret); // Change request = Same address and same port
String addr = serverAddress;
int port = serverPort;
if (!firstRun) {
addr = changedAddress.getAddress().getHostAddress();
port = changedAddress.getPort();
}
if (firstRun) bindingTestDone = DoneBindingTest.TEST1_FIRST_RUN;
else bindingTestDone = DoneBindingTest.TEST1_SECOND_RUN;
try {
byte buffer[] = sendReceive(header, addr, port);
header = MessageHeader.create(buffer);
} catch (SocketTimeoutException ex) {
if (firstRun) {
if (debug) System.out.println("Node is not capable of UDP communication.");
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.UDP_BLOCKED);
}
else {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
}
return false;
} catch (IOException ex) {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
MessageAttribute mappedAddress2 = null;
if (firstRun) {
mappedAddress = header.getMessageAttribute(MessageAttribute.MessageAttributeType.MAPPED_ADDRESS);
changedAddress = header.getMessageAttribute(MessageAttribute.MessageAttributeType.CHANGED_ADDRESS);
} else {
mappedAddress2 = header.getMessageAttribute(MessageAttribute.MessageAttributeType.MAPPED_ADDRESS);
if (mappedAddress2==null) {
discoveryInfo.setError(700, "The server is sending an incomplete response (Mapped Address and Changed Address message attributes are missing). The client should not retry.");
if (debug) System.out.println("Response does not contain a Mapped Address or Changed Address message attribute.");
return false;
}
}
MessageAttribute errorCode = header.getMessageAttribute(MessageAttribute.MessageAttributeType.ERROR_CODE);
if (errorCode!=null) {
if (debug) System.out.println("Got an error code from the STUN server");
discoveryInfo.setErrorCode(errorCode);
return false;
}
if (mappedAddress==null || changedAddress==null) {
discoveryInfo.setError(700, "The server is sending an incomplete response (Mapped Address and Changed Address message attributes are missing). The client should not retry.");
if (debug) System.out.println("Response does not contain a Mapped Address or Changed Address message attribute.");
return false;
}
if (!controlMessageIntegrity(header, sharedSecret)) {
discoveryInfo.setError(700, "Wrong HMAC received from server, this migh be an attack response");
return false;
}
if (firstRun) {
discoveryInfo.setPublicIpAddress(mappedAddress.getAddress());
discoveryInfo.setLocalIpAddress(localAddr);
if (mappedAddress.getPort()==localPort && mappedAddress.getAddressAsString().equals(localAddr.getHostAddress())) {
if (debug) System.out.println("Node is not natted.");
discoveryInfo.setNodeNated(false);
}
else {
if (debug) System.out.println("Node is natted.");
discoveryInfo.setNodeNated(true);
}
}
else {
//if (mappedAddress.getPort()!=mappedAddress2.getPort() || !mappedAddress.getAddress().equals(mappedAddress2.getAddress())) {
if (!mappedAddress.getAddress().equals(mappedAddress2.getAddress())) {
if (debug) System.out.println("Node is behind a symmetric NAT.");
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.SYMMETRIC_NAT);
return false;
}
}
return true;
}
private boolean test2(DiscoveryInfo discoveryInfo, SharedSecret sharedSecret) {
MessageHeader header = new MessageHeader(MessageHeader.HeaderType.BINDING_REQUEST);
header.genrateTransactionId();
//byte hmac[] = setMessageAttributes(header, (byte)6, sharedSecret); // Change request = Change addaress and port
bindingTestDone = DoneBindingTest.TEST2;
try {
byte buffer[] = sendReceive(header, serverAddress, serverPort);
header = MessageHeader.create(buffer);
} catch (SocketTimeoutException ex) {
if (discoveryInfo.isNodeNated()) {
return true;
} else {
if (debug) System.out.println("Node is behind a symmetric UDP firewall.");
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.SYMMETRIC_FIREWALL);
return false;
}
} catch (IOException ex) {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
MessageAttribute errorCode = header.getMessageAttribute(MessageAttribute.MessageAttributeType.ERROR_CODE);
if (errorCode!=null) {
if (debug) System.out.println("Got an error code from the STUN server");
discoveryInfo.setErrorCode(errorCode);
return false;
}
if (!controlMessageIntegrity(header, sharedSecret)) {
discoveryInfo.setError(700, "Wrong HMAC received from server, this migh be an attack response");
return false;
}
if (discoveryInfo.isNodeNated()) {
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.FULL_CONE_NAT);
if (debug) System.out.println("Node is behind a full-cone NAT.");
} else {
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.OPEN_INTERNET);
if (debug) System.out.println("Node has open access to the Internet (or, at least the node is behind a full-cone NAT without translation).");
}
return false;
}
private void test3(DiscoveryInfo discoveryInfo, SharedSecret sharedSecret) {
MessageHeader header = new MessageHeader(MessageHeader.HeaderType.BINDING_REQUEST);
header.genrateTransactionId();
//byte hmac[] = setMessageAttributes(header, (byte)2, sharedSecret); // Change request = Change port
bindingTestDone = DoneBindingTest.TEST3;
try {
byte buffer[] = sendReceive(header, serverAddress, serverPort);
header = MessageHeader.create(buffer);
} catch (SocketTimeoutException ex) {
if (discoveryInfo.isNodeNated()) {
if (debug) System.out.println("Node is behind a port restricted NAT.");
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.RESTRICTED_PORT_NAT);
return;
} else {
return;
}
} catch (IOException ex) {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
return;
}
MessageAttribute errorCode = header.getMessageAttribute(MessageAttribute.MessageAttributeType.ERROR_CODE);
if (errorCode!=null) {
if (debug) System.out.println("Got an error code from the STUN server");
discoveryInfo.setErrorCode(errorCode);
return;
}
if (!controlMessageIntegrity(header, sharedSecret)) {
discoveryInfo.setError(700, "Wrong HMAC received from server, this migh be an attack response");
return;
}
if (discoveryInfo.isNodeNated()) {
discoveryInfo.setScenario(DiscoveryInfo.ConnectionScenario.RESTRICTED_CORNE_NAT);
if (debug) System.out.println("Node is behind a restricted cone NAT.");
}
}
private byte[] setMessageAttributes(MessageHeader header, byte changeRequest, SharedSecret sharedSecret) {
MessageAttribute changeRequestAttr = MessageAttribute.create(MessageAttribute.MessageAttributeType.CHANGE_REQUEST, (int)changeRequest);
header.addMessageAttribute(changeRequestAttr);
byte hmac[] = null;
if (sharedSecret!=null) {
if (sharedSecret.getUsername()!=null) {
MessageAttribute usernameAttr = MessageAttribute.create(MessageAttribute.MessageAttributeType.USERNAME, sharedSecret.getUsername() ,0);
header.addMessageAttribute(usernameAttr);
}
if (sharedSecret.getPassword()!=null) try {
// The message Integrity must be the last attribute in the header and use
// The STUN message including the header, up to and including the attribute
// preceding to this attribute as text for the HMAC.
MessageAttribute messageIntegrityAttr = MessageAttribute.create(MessageAttribute.MessageAttributeType.MESSAGE_INTEGRITY, sharedSecret.getPassword(), header.toBytes());
header.addMessageAttribute(messageIntegrityAttr);
hmac = messageIntegrityAttr.getHMAC();
} catch (IOException ex) {
if (debug) Logger.getLogger(StunClient.class.getName()).log(Level.SEVERE, null, ex);
}
}
return hmac;
}
private boolean controlMessageIntegrity(MessageHeader header, SharedSecret sharedSecret) {
if (sharedSecret==null) return true;
if (header.integrityCheck(sharedSecret.getPassword())!=0) return false;
return true;
}
/**
*
* @return the enum @{link DoneBindingTest} value for the last test passed under the {@link StunClient#binding} method.
*/
public DoneBindingTest getBindingTestDone() {return bindingTestDone;}
/**
*
* @return a string description for the last test passed under the {@link StunClient#binding} method.
*/
public String getBindingTestDoneAsString() {
switch (bindingTestDone) {
case TEST1_FIRST_RUN: return "test 1 first run";
case TEST1_SECOND_RUN: return "test 1 second run";
case TEST2: return "test2";
case TEST3: return "test";
}
return "no test!";
}
private byte[] sendReceive(MessageHeader header, String address, int port) throws IOException {
InetAddress addr = InetAddress.getByName(address);
DatagramSocket sock = new DatagramSocket(5060);
sock.setReuseAddress(true);
int timeout = 100;
int timeSinceFirstTransmission = 0;
DatagramPacket retDatagramPacket = null;
try {
byte buffer[] = header.toBytes();
DatagramPacket out = new DatagramPacket(buffer, buffer.length, addr, port);
while (true) {
sock.send(out);
byte[] buf = new byte[0xffff+20];
retDatagramPacket = new DatagramPacket(buf, buf.length);
sock.setSoTimeout(timeout);
try {
sock.receive(retDatagramPacket);
} catch (SocketTimeoutException ex) {
if (timeout<1600) timeout = timeout*2;
timeSinceFirstTransmission += timeout;
if (timeSinceFirstTransmission > 9500) throw ex;
continue;
}
break;
}
//localAddr = sock.getLocalAddress();
//localAddr = InetAddress.getLocalHost();
localAddr = Utils.getLocalAddr();
// System.out.println(localAddr);
// System.out.println(localAddr.getLocalHost());
// System.out.println(localAddr.getHostAddress());
localPort = sock.getLocalPort();
} finally {
sock.close();
}
return retDatagramPacket.getData();
}
}