/*
* Created on 12-Jun-2006
* Created by Marc Colosimo
* Copyright (C) 2004, 2005, 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 46,603.30 euros
* 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
*
* Connection class for NAT-PMP (Port Mapping Protocol) Devices
*
* @see <http://files.dns-sd.org/draft-cheshire-nat-pmp.txt>
* Tested with <https://www.grc.com/x/portprobe=6881>
*
* This code is ugly, but it works.
*
* Some assumptions:
* - The NAT device will be at xxx.xxx.xxx.1
*
* This needs to be threaded.
* - It could take upto 2 minutes to timeout during any request
* - We need to listen for address changes (using link-local multicast?)!
* - We need to request the mapping again before it expires
*
* Some hints and to dos:
* - The draft spec says that the device could set max lease life time
* to be less than requested. this should be checked.
* - Need to make something to renew port mappings - recommend that
* the client SHOULD begin trying to renew the mapping halfway to *
* expiry time, like DHCP
* - Need to listen for public address changes
*
* Version 0.1b
*/
package com.aelitis.net.natpmp.impl;
import java.net.*;
import com.aelitis.azureus.core.util.NetUtils;
import com.aelitis.net.natpmp.NATPMPDeviceAdapter;
import com.aelitis.net.natpmp.NatPMPDevice;
/**
*
* Main class
*
* */
public class NatPMPDeviceImpl implements NatPMPDevice
{
static final int NATMAP_VER = 0;
static final int NATMAP_PORT = 5351;
static final int NATMAP_RESPONSE_MASK = 128;
static final int NATMAP_INIT_RETRY = 250; // ms
static final int NATMAP_MAX_RETRY = 2250; // gives us three tries
// lease life in seconds
// 24 hours
static final int NATMAP_DEFAULT_LEASE = 60*60*24;
// link-local multicast address - for address changes
// not implemented
static final String NATMAP_LLM = "224.0.0.1";
// Opcodes used for ..
static final byte NATOp_AddrRequest = 0; // Ask for a NAT-PMP device
static final byte NATOp_MapUDP = 1; // Map a UDP Port
static final byte NATOp_MapTCP = 2; // Map a TCP Port
/* Length of Requests in bytes */
static final int NATAddrRequest = 2;
static final int NATPortMapRequestLen = 4 * 3; // 4 bytes by 3
/* Length of Replies in Bytes */
static final int NATAddrReplyLen = 4 * 3;
static final int NATPortMapReplyLen = 4 * 4;
/* Current Result Codes */
static final int NATResultSuccess = 0;
static final int NATResultUnsupportedVer = 1;
/**
* Not Authorized/Refused
* (e.g. box supports mapping, but user has turned feature off)
**/
static final int NATResultNotAuth = 2;
/**
* Network Failure
* (e.g. NAT box itself has not obtained a DHCP lease)
**/
static final int NATResultNetFailure = 3;
static final int NATResultNoResc = 4; // Out of resources
static final int NATResultUnsupportedOp = 5; // Unsupported opcode
/* Instance specific globals */
String current_router_address = "?";
InetAddress hostInet; // Our address
InetAddress natPriInet; // NAT's private (interal) address
InetAddress natPubInet; // NAT's public address
NetworkInterface networkInterface; // natPriInet network interface
InetAddress llmInet;
boolean nat_pmp_found = false;
int nat_epoch = 0; // This gets updated each request
private NATPMPDeviceAdapter adapter;
/**
* Singleton creation
**/
private static NatPMPDeviceImpl NatPMPDeviceSingletonRef;
public static synchronized NatPMPDeviceImpl
getSingletonObject(NATPMPDeviceAdapter adapter) throws Exception {
if (NatPMPDeviceSingletonRef == null)
NatPMPDeviceSingletonRef = new NatPMPDeviceImpl(adapter);
return NatPMPDeviceSingletonRef;
}
private
NatPMPDeviceImpl(
NATPMPDeviceAdapter _adapter)
throws Exception
{
adapter = _adapter;
hostInet = NetUtils.getLocalHost();
checkRouterAddress();
}
protected void
checkRouterAddress()
throws Exception
{
String natAddr = adapter.getRouterAddress().trim();
if ( natAddr.length() == 0 ){
natAddr = convertHost2RouterAddress(hostInet);
}
if ( natAddr.equals( current_router_address )){
return;
}
current_router_address = natAddr;
log("Using Router IP: " + natAddr);
natPriInet = InetAddress.getByName(natAddr);
networkInterface = NetworkInterface.getByInetAddress( natPriInet );
}
/**
* Send a request and wait for reply
* This class should be threaded!!!
*
* This sends to the default NATPMP_PORT.
*
* @param dstInet destination address (should be the private NAT address)
* @param dstPkt packet to send
* @param recBuf byte buffer big enough to hold received
**/
public DatagramPacket sendNATMsg(InetAddress dstInet, DatagramPacket dstPkt, byte[] recBuf) throws Exception {
int retryInterval = NATMAP_INIT_RETRY;
boolean recRep = false;
DatagramSocket skt = new DatagramSocket();
skt.connect( dstInet, NATMAP_PORT );
skt.setSoTimeout( NATMAP_INIT_RETRY );
skt.send(dstPkt); // how do we know we hit something?
DatagramPacket recPkt = new DatagramPacket(recBuf, recBuf.length);
// We have several tries at this (like 3)
while ( !recRep && (retryInterval < NATMAP_MAX_RETRY) ) {
try {
skt.receive(recPkt);
recRep = true;
} catch (SocketTimeoutException ste) {
//log("Timed Out!");
//log( ste.getMessage() );
// sleep before trying again
// this.sleep(retryInterval);
Thread.sleep(retryInterval); // not sleeping?!?
// increase retry interval
retryInterval += (retryInterval * 2);
}
}
if ( !recRep ){
throw( new PortUnreachableException());
}
// check recRep for true!!!
return recPkt;
}
/**
* Try to connect with a NAT-PMP device.
* This could take sometime.
*
* @return true if it found one
**/
public boolean connect() throws Exception {
checkRouterAddress();
try{
// Send NAT request to find out if it is PMP happy
byte reqBuf[] = {NATMAP_VER, NATOp_AddrRequest};
DatagramPacket dstPkt = new DatagramPacket(reqBuf, reqBuf.length);
byte recBuf[] = new byte[NATAddrReplyLen];
/* DatagramPacket recPkt = */ sendNATMsg(natPriInet, dstPkt, recBuf);
//int recVer = unsigned8ByteArrayToInt( recBuf, 0 );
//int recOp = unsigned8ByteArrayToInt( recBuf, 1 );
int recErr = unsigned16ByteArrayToInt( recBuf, 2 );
int recEpoch = unsigned32ByteArrayToInt( recBuf, 4 );
String recPubAddr = unsigned8ByteArrayToInt( recBuf, 8 ) + "." +
unsigned8ByteArrayToInt( recBuf, 9 ) + "." +
unsigned8ByteArrayToInt( recBuf, 10 ) + "." +
unsigned8ByteArrayToInt( recBuf, 11 );
/* set the global NAT public address */
natPubInet = InetAddress.getByName(recPubAddr);
/* set the global NAT Epoch time (in seconds) */
nat_epoch = recEpoch;
if (recErr != 0)
throw( new Exception("NAT-PMP connection error: " + recErr) );
log("Err: " +recErr);
log("Uptime: " + recEpoch);
log("Public Address: " + recPubAddr);
/**
* TO DO:
* Set up listner for announcements from the device for
* address changes (public address changes)
**/
nat_pmp_found = true;
return true;
}catch( PortUnreachableException e ){
return( false );
}
}
/**
* Asks for a public port to be mapped to a private port from this host.
*
* NAP-PMP allows the device to assign another public port if the
* requested one is taken. So, you should check the returned port.
*
* @param tcp true TCP, false UDP
* @return the returned publicPort. -1 if error occured
* @todo either take a class (like UPnPMapping) or return a class
**/
public int addPortMapping( boolean tcp, int publicPort,
int privatePort ) throws Exception {
// check for actual connection!
return portMappingProtocol( tcp, publicPort, privatePort,
NATMAP_DEFAULT_LEASE );
}
/**
* Delete a mapped public port
*
* @param tcp true TCP, false UDP port
* @param publicPort the public port to close
* @param privatePort the private port that it is mapped to
* @warn untested
*/
public void deletePortMapping( boolean tcp, int publicPort,
int privatePort )
throws Exception {
/**
* if the request was successful, a zero lifetime will
* delete the mapping and return a public port of 0
**/
// check for actual connection
/*int result = */ portMappingProtocol(tcp, publicPort, privatePort, 0);
}
/**
* General port mapping protocol
*
*
*
**/
public int portMappingProtocol( boolean tcp, int publicPort,
int privatePort, int lifetime )
throws Exception {
byte NATOp = (tcp?NATOp_MapTCP:NATOp_MapUDP);
// Should check for errors - only using lower 2 bytes
byte pubPort[] = intToByteArray(publicPort);
byte priPort[] = intToByteArray(privatePort);
byte portLifeTime[] = intToByteArray(lifetime);
// Generate Port Map request packet
byte dstBuf[] = new byte[NATPortMapRequestLen];
dstBuf[0] = NATMAP_VER; // Ver
dstBuf[1] = NATOp; // OP
dstBuf[2] = 0; // Reserved - 2 bytes
dstBuf[3] = 0;
dstBuf[4] = priPort[2]; // Private Port - 2 bytes
dstBuf[5] = priPort[3];
dstBuf[6] = pubPort[2]; // Requested Public Port - 2 bytes
dstBuf[7] = pubPort[3];
for (int i = 0; i < 4; i++) {
dstBuf[8 + i] = portLifeTime[i];
}
DatagramPacket dstPkt = new DatagramPacket(dstBuf, dstBuf.length);
byte recBuf[] = new byte[NATPortMapReplyLen];
/* DatagramPacket recPkt = */ sendNATMsg(natPriInet, dstPkt, recBuf);
// Unpack this and check codes
//int recVers = unsigned8ByteArrayToInt( recBuf, 0 );
int recOP = unsigned8ByteArrayToInt( recBuf, 1 );
int recCode = unsigned16ByteArrayToInt( recBuf, 2 );
int recEpoch = unsigned32ByteArrayToInt( recBuf, 4);
//int recPriPort = unsigned16ByteArrayToInt( recBuf, 8 );
int recPubPort = unsigned16ByteArrayToInt( recBuf, 10 );
int recLifetime = unsigned32ByteArrayToInt( recBuf, 12);
/**
* Should save the epoch. This can be used to determine the
* time the mapping will be deleted.
**/
log("Seconds since Start of Epoch: " + recEpoch);
log("Returned Mapped Port Lifetime: " + recLifetime);
if ( recCode != 0 )
throw( new Exception( "An error occured while getting a port mapping: " + recCode ) );
if ( recOP != ( NATOp + 128) )
throw( new Exception( "Received the incorrect port type: " + recOP) );
if ( lifetime != recLifetime )
log("Received different port life time!");
return recPubPort;
}
public InetAddress
getLocalAddress()
{
return( hostInet );
}
public NetworkInterface
getNetworkInterface()
{
return( networkInterface );
}
public String
getExternalIPAddress()
{
return( natPubInet.getHostAddress());
}
public int
getEpoch()
{
return( nat_epoch );
}
protected void
log(
String str )
{
adapter.log( str );
}
/**
*
* Bunch of conversion functions
*
**/
/**
* Convert the byte array containing 32-bit to an int starting from
* the given offset.
*
* @param b The byte array
* @param offset The array offset
* @return The integer
*/
public static int unsigned32ByteArrayToInt(byte[] b, int offset) {
int value = 0;
for (int i = 0; i < 4; i++) {
int shift = (4 - 1 - i) * 8;
value += ( (int) b[i + offset] & 0xFF) << shift;
}
return value;
}
/**
* Convert the byte array containing 16-bits to an int starting from
* the given offset.
*
* @param b The byte array
* @param offset The array offset
* @return The integer
*/
public static int unsigned16ByteArrayToInt(byte[] b, int offset) {
int value = 0;
for (int i = 0; i < 2; i++) {
int shift = (2 - 1 - i) * 8;
value += ( (int) b[i + offset] & 0xFF) << shift;
}
return value;
}
/**
* Convert the byte array containing 8-bits to an int starting from
* the given offset.
*
* @param b The byte array
* @param offset The array offset
* @return The integer
*/
public static int unsigned8ByteArrayToInt(byte[] b, int offset) {
return (int) b[offset] & 0xFF;
}
public short unsignedByteArrayToShort(byte[] buf) {
if (buf.length == 2) {
int i;
i = ( ( ( (int) buf[0] & 0xFF) << 8) | ( (int) buf[1] & 0xFF) );
return (short) i;
}
return -1;
}
/**
* Convert a 16-bit short into a 2 byte array
*
* @return unsigned byte array
**/
public byte[] shortToByteArray(short v) {
byte b[] = new byte[2];
b[0] = (byte) ( 0xFF & (v >> 8) );
b[1] = (byte) ( 0xFF & (v >> 0) );
return b;
}
/**
* Convert a 32-bit int into a 4 byte array
*
* @return unsigned byte array
**/
public byte[] intToByteArray(int v) {
byte b[] = new byte[4];
int i, shift;
for(i = 0, shift = 24; i < 4; i++, shift -= 8)
b[i] = (byte)(0xFF & (v >> shift));
return b;
}
public String intArrayString(int[] buf) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < buf.length; i++) {
sb.append(buf[i]).append(" ");
}
return sb.toString();
}
public String byteArrayString(byte[] buf) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < buf.length; i++) {
sb.append(buf[i]).append(" ");
}
return sb.toString();
}
/**
*
* @param init takes the host address
* @return String the address as (xxx.xxx.xxx.1)
**/
private String convertHost2RouterAddress(InetAddress inet) {
byte rawIP[] = inet.getAddress();
// assume router is at xxx.xxx.xxx.1
rawIP[3] = 1;
// is there no printf in java?
String newIP = (rawIP[0]&0xff) +"."+(rawIP[1]&0xff)+"."+(rawIP[2]&0xff)+"."+(rawIP[3]&0xff);
return newIP;
}
}