package edu.uw.cse.netlab.reputation; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketException; import java.security.PublicKey; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.Set; import java.util.logging.Logger; import org.gudy.azureus2.core3.config.COConfigurationManager; import org.gudy.azureus2.core3.util.AEThread2; import org.gudy.azureus2.core3.util.ByteFormatter; import com.aelitis.net.udp.uc.PRUDPPacketHandler; import com.aelitis.net.udp.uc.PRUDPPacketHandlerFactory; import com.aelitis.net.udp.uc.impl.PRUDPPacketHandlerImpl; import com.aelitis.net.udp.uc.impl.ExternalUdpPacketHandler; import edu.uw.cse.netlab.reputation.messages.Attestation; import edu.uw.cse.netlab.reputation.storage.Receipt; import edu.uw.cse.netlab.reputation.storage.ReputationDAO; import edu.uw.cse.netlab.reputation.storage.SoftStateListener; import edu.uw.cse.netlab.utils.BloomFilter; import edu.uw.cse.netlab.utils.ByteManip; import edu.uw.cse.netlab.utils.CoreWaiter; import edu.uw.cse.netlab.utils.KeyManipulation; class UpdateReceiptWrapper implements Serializable { private static final long serialVersionUID = 1L; public long seq; public Receipt receipt; public UpdateReceiptWrapper( long inSeq, Receipt inReceipt ) { receipt = inReceipt; seq = inSeq; } } class SentUpdate { // Who did we ask public PublicKey intermediary; // Who did we ask about public PublicKey receiver; // For timing out short-term attempts public long time_sent; // these are SHORT TERM attempts to combat packet loss rather than actual downtime public short attempts; /** * This is the attested receipt: i.e., the (potentially outdated) receipt from the intermediary that was forwarded to us * by the would-be receiver */ public Receipt receipt; static private long seq = 0; // for multiple outstanding intermediary <-> receiver reqs public long our_seq = 0; public long attest_id = -1; public SentUpdate(PublicKey inIntermediary, PublicKey inReceiver, Receipt inReceipt, Long attest_id) { our_seq = seq++; intermediary = inIntermediary; receiver = inReceiver; time_sent = System.currentTimeMillis(); attempts = 0; receipt = inReceipt; this.attest_id = attest_id.longValue(); } @Override public boolean equals( Object rhs ) { if( rhs instanceof SentUpdate ) { SentUpdate r = (SentUpdate)rhs; return r.intermediary.equals(intermediary) && r.receiver.equals(receiver) && r.time_sent == time_sent && r.attempts == attempts; } return false; } } public class UpdatePacketHandler extends CoreWaiter implements ExternalUdpPacketHandler { public static final byte [] MAGIC = new byte[]{(byte)0, (byte)0, (byte)0, (byte)211, (byte)23}; private static Logger logger = Logger.getLogger(UpdatePacketHandler.class.getName()); private DatagramSocket mSocket; private static UpdatePacketHandler mInst = null; public synchronized static UpdatePacketHandler get() { if( mInst == null ) mInst = new UpdatePacketHandler(); return mInst; } List<SentUpdate> outstanding_updates; /** * This thread periodically checks for updates from the DB that need * verified at intermediaries and sends them off. */ AEThread2 mUpdateScanner; private UpdatePacketHandler() { super(); } @Override protected void init() { logger.info("update packet handler init()"); outstanding_updates = Collections.synchronizedList(new LinkedList<SentUpdate>()); mUpdateScanner = new AEThread2("update scanner", true) { @Override public void run() { ReputationDAO rep = ReputationDAO.get(); long last_packet = System.currentTimeMillis(); while( true ) { try { rep.get_attestation_for_verification(); if( rep != null ) { Thread.sleep(30*1000); continue; } } catch( Exception e ) { e.printStackTrace(); } logger.fine("receipt verification scanner is running..."); try { SentUpdate update = null; /** * First, try to clear / timeout any existing updates that are outstanding */ List<SentUpdate> to_remove = new LinkedList<SentUpdate>(); synchronized(outstanding_updates) { for( SentUpdate candidate : outstanding_updates ) { logger.fine("inspecting outstanding update to: " + KeyManipulation.concise(candidate.intermediary.getEncoded())); // Timeout check // TODO: magic constant: 2 second timeout on UDP messages. if( candidate.time_sent != 0 && (candidate.time_sent + 2000) < System.currentTimeMillis() ) { candidate.attempts++; candidate.time_sent = 0; logger.fine("timeout"); } // Intermediary presumed unavailable // TODO: magic constant: 3 retries for UDP send. if( candidate.attempts == 3 ) { to_remove.add(candidate); continue; } // This message is still outstanding so don't resent request if( candidate.time_sent != 0 ) continue; // At this point, we have something to retry (either we completed an IP lookup or its time to retry) update = candidate; logger.fine("found update to verify in outstanding updates"); } } // prune dead entries for( SentUpdate dead : to_remove ) { // Two possible reasons this failed: 1) host actually unavailable or 2) out of date IP mapping. // Here register a check to the mapping. Eventually we will long-term retry this update. rep.getSoftStateSync().refreshRemoteID(dead.intermediary, null); outstanding_updates.remove(dead); } // If nothing to do locally, queue up a new update from the DB if any exist. if( update == null ) { Object [] tuple = rep.get_attestation_for_verification(); Receipt to_verify = (Receipt)tuple[0]; Long which_intermediary = (Long)tuple[1]; Long attest_id = (Long)tuple[2]; // Absolutely nothing to verify, wait a while before trying again. (DEBUG -- don't) if( to_verify == null ) { logger.finest("nothing to verify, sleeping..."); try { // TODO: magic constant Thread.sleep(30*1000); } catch( Exception e ) {} continue; } logger.fine("got receipt to verify: " + to_verify); outstanding_updates.add(new SentUpdate( rep.get_public_key(which_intermediary), to_verify.getEncodingStateForKey(), to_verify, attest_id)); } else { final SentUpdate update_shadow = update; logger.fine("executing update: " + update + " checking soft state" + "(" + update_shadow.hashCode() + ")" ); /** * First, do we have this intermediary's IP _at all_? */ String sstate = rep.get_soft_state(update_shadow.intermediary); logger.finer("intermediary key: " + ByteFormatter.encodeString(update_shadow.intermediary.getEncoded())); if( sstate == null ) { logger.finer("didn't have soft state, attempting refresh and removing from outstanding updates: " + KeyManipulation.concise(update.intermediary.getEncoded())); // no need to try this 3 times and lodge 3 dht lookups outstanding_updates.remove(update); rep.getSoftStateSync().refreshRemoteID(update_shadow.intermediary, new SoftStateListener(){ @Override public void refresh_complete( PublicKey inID ) { // promote update to the front of the list synchronized(outstanding_updates) { logger.finer("soft state refresh finished, reinserting update into outstanding updates: " + KeyManipulation.concise(update_shadow.intermediary.getEncoded()) + " (" + update_shadow.hashCode() + ")"); outstanding_updates.remove(update_shadow); update_shadow.time_sent = 0; outstanding_updates.add(0, update_shadow); } } }); continue; } logger.finer("got soft state"); InetAddress addr = InetAddress.getByName(sstate.split("\\s+")[0]); int port = Integer.parseInt(sstate.split("\\s+")[1]); UpdateReceiptWrapper out = new UpdateReceiptWrapper( update_shadow.our_seq, update_shadow.receipt ); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(MAGIC); baos.write(ByteManip.objectToBytes(out)); byte [] receipt_bytes = baos.toByteArray(); update.attempts++; DatagramPacket p = new DatagramPacket( receipt_bytes, receipt_bytes.length, addr, port ); /** * Rate limit -- 1 packet / sec * TODO: magic constant: rate limit 1/sec */ try { Thread.sleep(1000 - (System.currentTimeMillis()-last_packet)); } catch( Exception e ) {} logger.fine("******* sending verify receipt packet ********** length: " + receipt_bytes.length + " to " + addr + " port " + port); mSocket.send(p); update.time_sent = last_packet = System.currentTimeMillis(); } } catch( Exception e ) { e.printStackTrace(); logger.warning("Update scanner: " + e.toString()); try { Thread.sleep(1000); } catch( Exception e2 ) {} } } } // run }; // mUpdateScanner def logger.finest("getting udp port handler"); // Get the socket for the udp.listen.port PRUDPPacketHandlerImpl handler = (PRUDPPacketHandlerImpl)(PRUDPPacketHandlerFactory.getHandler(COConfigurationManager.getIntParameter("UDP.Listen.Port"))); handler.addExternalHandler(this); mSocket = handler.getSocket(); logger.finest("got handler on port: " + mSocket.getLocalPort() + " sock is: " + mSocket); logger.fine("starting update scanner thread"); mUpdateScanner.start(); } /** * * This should be called only by our modified UDP packet handler. We might receive here from * any UDP port so we can't rely only on the socket we have here... * * @param packet the packet to check * @return true if this was an update packet that we processed, false otherwise */ @Override public boolean packetReceived( DatagramPacket packet ) { byte [] data = packet.getData(); for( int i=0; i<MAGIC.length; i++ ) if( MAGIC[i] != data[i] ) return false; logger.finer("******** packet received, passed magic header check ********** " + " port " + packet.getPort() + " " + packet.getLength() + " " + packet.getData().length); ByteArrayInputStream bais = new ByteArrayInputStream(data); bais.skip(MAGIC.length); try { ObjectInputStream ois = new ObjectInputStream(bais); UpdateReceiptWrapper r = (UpdateReceiptWrapper)ois.readObject(); if( isUpdate(r) ) receiveUpdate(r, packet); else { SentUpdate s = isAcknowledgement(r); if( s != null ) receiveAck(s, r); else throw new IOException("neither an update nor an ack receipt: " + r); } return true; } catch( Exception e ) { logger.warning("Error parsing update message (after magic number): " + e.toString()); } return false; } private void receiveAck( SentUpdate inUpdate, UpdateReceiptWrapper r ) throws IOException { logger.finer("processing acknowledgement from " + KeyManipulation.concise(inUpdate.intermediary.getEncoded())); /** * We need to make sure the intermediary's state is >= the claimed standing by the remote peer * (otherwise, either the remote peer is replaying receipts from the intermediary too many times or the * intermediary is lying) */ Receipt reported_by_intermediary = r.receipt; Receipt given_by_peer = inUpdate.receipt; long received_due_to_reco_offset = given_by_peer.get_received_due_to_reco_offset(); // TODO: add more sanity checks here -- data directly sent/received, etc. /** * For now, we'll assume that the intermediary will at least be consistent with itself (i.e. direct received strictly increasing -- but we should check this in the future) * and instead check for the repeated use of old receipts. */ if( given_by_peer.get_peer_received_due_to_reco() - received_due_to_reco_offset > reported_by_intermediary.get_peer_received_due_to_reco() ) { // TODO: We've possibly detected cheating here. If we want to add some penalty for this, here is the place logger.warning("possible replay / accounting irregularity: " + given_by_peer.get_peer_received_due_to_reco() + " " + received_due_to_reco_offset + " " + reported_by_intermediary.get_peer_received_due_to_reco() ); } else logger.finer("passed check, given by peer: " + given_by_peer.get_peer_received_due_to_reco() + " / offset: " +received_due_to_reco_offset + " reported by int: " + reported_by_intermediary.get_peer_received_due_to_reco()); ReputationDAO.get().attestation_verification_complete(inUpdate.attest_id); } private SentUpdate isAcknowledgement( UpdateReceiptWrapper r ) { PublicKey intermediary = r.receipt.getSigningKey(); PublicKey regarding = r.receipt.getEncodingStateForKey(); synchronized(outstanding_updates) { for( SentUpdate s : outstanding_updates ) { if( s.intermediary.equals(intermediary) && s.receiver.equals(regarding) && r.seq == s.our_seq ) { return s; } } } return null; } private boolean isUpdate( UpdateReceiptWrapper r ) { BloomFilter bf = r.receipt.getOnBehalfOf(); if( bf == null ) return false; return bf.test(LocalIdentity.get().getKeys().getPublic().getEncoded()); } public void receiveUpdate( UpdateReceiptWrapper r, DatagramPacket packet ) { ReputationDAO db = ReputationDAO.get(); try { /** * If this update has already been applied, discard */ if( db.is_duplicate_update(r.receipt) ) { // TODO: punish replay attack? -- more likely just a retransmit due to loss. fall through and send ack again here. logger.warning("Seemingly a replay attack (or retransmit due to loss) from receipt: " + r); } else { /** * Apply update to persistent storage. This might fail if: * - the sender is not faithful to his attestation and * - the bloom filter allows a false positive with us and * - we don't have any local state for this peer * * TODO: make this transactional? */ if( r.receipt.getOnBehalfOf().getStoredCount() == -1 ) throw new IOException("attesting receipt not correctly formatted, -1 stored count in onBehalfOf"); logger.finer("received update, sent_direct_diff is: " + r.receipt.get_sent_direct_diff() + " stored count is: " + r.receipt.getOnBehalfOf().getStoredCount()); db.others_recv_due_to_my_reco(r.receipt.getEncodingStateForID(), r.receipt.get_sent_direct_diff() / r.receipt.getOnBehalfOf().getStoredCount()); db.others_sent_due_to_my_reco(r.receipt.getSigningID(), r.receipt.get_sent_direct_diff() / r.receipt.getOnBehalfOf().getStoredCount() ); db.record_processed_update(r.receipt); // TODO: remove this, we record this information now only for debugging db.record_update(r.receipt); logger.fine("applied received update: " + (r.receipt.get_sent_direct_diff() / r.receipt.getOnBehalfOf().getStoredCount()) ); } } catch( IOException e ) { System.err.println("Error applying update: " + e); e.printStackTrace(); return; } /* * next, need to ack this with a receipt indicating the state of the receiver * from our perspective. This receipt doesn't include any on behalf of info * and, since it includes state from us -> receiver, the sender (who will receive it * for verification) can't really use it for anything other than verification */ try { UpdateReceiptWrapper ack = new UpdateReceiptWrapper(r.seq, new Receipt(r.receipt.getEncodingStateForKey(), null, true)); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(MAGIC); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(ack); byte [] bytes = baos.toByteArray(); mSocket.send(new DatagramPacket(bytes, bytes.length, packet.getAddress(), packet.getPort())); logger.fine("sent ack to: " + packet.getAddress().toString() + " port " + packet.getPort()); } catch( IOException e ) { logger.warning("couldn't send update ack: " + e); e.printStackTrace(); return; } logger.fine("applied received update " + r); } public static final void main( String [] args ) throws Exception { UpdatePacketHandler p = UpdatePacketHandler.get(); FileInputStream fis = new FileInputStream("verifypacket21975073"); byte [] b = new byte[fis.available()]; fis.read(b); p.packetReceived(new DatagramPacket(b, b.length)); } @Override public void socketUpdated(DatagramSocket socket) { this.mSocket = socket; } }