/* * 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; } }