diff --git a/history.txt b/history.txt
index ede3f8757..ef97851b9 100644
--- a/history.txt
+++ b/history.txt
@@ -1,7 +1,12 @@
-$Id: history.txt,v 1.241 2005/09/07 17:32:06 jrandom Exp $
+$Id: history.txt,v 1.242 2005/09/09 20:13:50 cervantes Exp $
+
+2005-09-09 jrandom
+ * Added preliminary support for NAT hole punching through SSU introducers
+ * Honor peer test results from peers that we have an SSU session with if
+ those sessions are idle for 3 minutes or more.
2005-09-09 cervantes
- * New build due to change in build number :P (thanks ugha!)
+ * New build due to change in build number :P (thanks ugha!)
2005-09-07 BarkerJr
* HTML cleanup for the router console (thanks!)
diff --git a/router/doc/udp.html b/router/doc/udp.html
index 05a5ef0f4..516b41532 100644
--- a/router/doc/udp.html
+++ b/router/doc/udp.html
@@ -1,4 +1,4 @@
-$Id: udp.html,v 1.15 2005/08/03 13:58:13 jrandom Exp $
+$Id: udp.html,v 1.16 2005/08/17 15:05:02 jrandom Exp $
Indirect session establishment by means of a third party introduction +is necessary for efficient NAT traversal. Charlie, a router behind a +NAT or firewall which does not allow unsolicited inbound UDP packets, +first contacts a few peers, choosing some to serve as introducers. Each +of these peers (Bob, Bill, Betty, etc) provide Charlie with an introduction +tag - a 4 byte random number - which he then makes available to the public +as methods of contacting him. Alice, a router who has Charlie's published +contact methods, first sends a RelayRequest packet to one or more of the +introducers, asking each to introduce her to Charlie (offering the +introduction tag to identify Charlie). Bob then forwards a RelayIntro +packet to Charlie including Alice's public IP and port number, then sends +Alice back a RelayResponse packet containing Charlie's public IP and port +number. When Charlie receives the RelayIntro packet, he sends off a small +random packet to Alice's IP and port (poking a hole in his NAT/firewall), +and when Alice receive's Bob's RelayResponse packet, she begins a new +full direction session establishment with the specified IP and port.
+ + +The automation of collaborative reachability testing for peers is diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 9d5f3b575..bcb2539d0 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -15,9 +15,9 @@ import net.i2p.CoreVersion; * */ public class RouterVersion { - public final static String ID = "$Revision: 1.228 $ $Date: 2005/09/07 17:31:13 $"; + public final static String ID = "$Revision: 1.229 $ $Date: 2005/09/09 20:13:49 $"; public final static String VERSION = "0.6.0.5"; - public final static long BUILD = 3; + public final static long BUILD = 4; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java index 1efc91fa6..62acf6256 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -1,6 +1,7 @@ package net.i2p.router.transport.udp; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -8,15 +9,18 @@ import java.util.Iterator; import java.util.Map; import net.i2p.crypto.DHSessionKeyBuilder; +import net.i2p.data.Base64; import net.i2p.data.RouterAddress; import net.i2p.data.RouterIdentity; import net.i2p.data.SessionKey; import net.i2p.data.Signature; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.router.CommSystemFacade; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleTimer; /** * Coordinate the establishment of new sessions - both inbound and outbound. @@ -35,6 +39,8 @@ public class EstablishmentManager { private Map _outboundStates; /** map of RemoteHostId to List of OutNetMessage for messages exceeding capacity */ private Map _queuedOutbound; + /** map of nonce (Long) to OutboundEstablishState */ + private Map _liveIntroductions; private boolean _alive; private Object _activityLock; private int _activity; @@ -50,11 +56,15 @@ public class EstablishmentManager { _inboundStates = new HashMap(32); _outboundStates = new HashMap(32); _queuedOutbound = new HashMap(32); + _liveIntroductions = new HashMap(32); _activityLock = new Object(); _context.statManager().createRateStat("udp.inboundEstablishTime", "How long it takes for a new inbound session to be established", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); _context.statManager().createRateStat("udp.outboundEstablishTime", "How long it takes for a new outbound session to be established", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); _context.statManager().createRateStat("udp.inboundEstablishFailedState", "What state a failed inbound establishment request fails in", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); _context.statManager().createRateStat("udp.outboundEstablishFailedState", "What state a failed outbound establishment request fails in", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); + _context.statManager().createRateStat("udp.sendIntroRelayRequest", "How often we send a relay request to reach a peer", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); + _context.statManager().createRateStat("udp.sendIntroRelayTimeout", "How often a relay request times out before getting a response (due to the target or intro peer being offline)", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); + _context.statManager().createRateStat("udp.receiveIntroRelayResponse", "How long it took to receive a relay response", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); } public void startup() { @@ -134,7 +144,7 @@ public class EstablishmentManager { } else { state = new OutboundEstablishState(_context, remAddr, port, msg.getTarget().getIdentity(), - new SessionKey(addr.getIntroKey())); + new SessionKey(addr.getIntroKey()), addr); _outboundStates.put(to, state); } } @@ -155,15 +165,28 @@ public class EstablishmentManager { * */ void receiveSessionRequest(RemoteHostId from, UDPPacketReader reader) { + boolean isNew = false; InboundEstablishState state = null; synchronized (_inboundStates) { state = (InboundEstablishState)_inboundStates.get(from); if (state == null) { state = new InboundEstablishState(_context, from.getIP(), from.getPort(), _transport.getLocalPort()); + isNew = true; _inboundStates.put(from, state); } } state.receiveSessionRequest(reader.getSessionRequestReader()); + if (isNew) { + if (!_transport.introducersRequired()) { + long tag = _context.random().nextLong(MAX_TAG_VALUE); + state.setSentRelayTag(tag); + if (_log.shouldLog(Log.INFO)) + _log.info("Received session request from " + from + ", sending relay tag " + tag); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Received session request, but our status is " + _transport.getReachabilityStatus()); + } + } if (_log.shouldLog(Log.DEBUG)) _log.debug("Receive session request from: " + state.getRemoteHostId().toString()); @@ -265,7 +288,7 @@ public class EstablishmentManager { OutboundEstablishState qstate = new OutboundEstablishState(_context, remAddr, port, msg.getTarget().getIdentity(), - new SessionKey(addr.getIntroKey())); + new SessionKey(addr.getIntroKey()), addr); _outboundStates.put(to, qstate); for (int i = 0; i < queued.size(); i++) @@ -304,10 +327,10 @@ public class EstablishmentManager { peer.setLastSendTime(now); peer.setRemoteAddress(state.getSentIP(), state.getSentPort()); peer.setRemotePeer(remote.calculateHash()); - if (true) // for now, only support direct - peer.setRemoteRequiresIntroduction(false); - peer.setTheyRelayToUsAs(0); peer.setWeRelayToThemAs(state.getSentRelayTag()); + peer.setTheyRelayToUsAs(0); + //if (true) // for now, only support direct + // peer.setRemoteRequiresIntroduction(false); _transport.addRemotePeerState(peer); @@ -334,8 +357,6 @@ public class EstablishmentManager { peer.setLastSendTime(now); peer.setRemoteAddress(state.getSentIP(), state.getSentPort()); peer.setRemotePeer(remote.calculateHash()); - if (true) // for now, only support direct - peer.setRemoteRequiresIntroduction(false); peer.setTheyRelayToUsAs(state.getReceivedRelayTag()); peer.setWeRelayToThemAs(0); @@ -364,10 +385,19 @@ public class EstablishmentManager { _transport.send(m, peer); } + public static final long MAX_TAG_VALUE = 0xFFFFFFFFl; + private void sendCreated(InboundEstablishState state) { long now = _context.clock().now(); - if (true) // for now, don't offer to relay + if (!_transport.introducersRequired()) { + // offer to relay + // (perhaps we should check our bw usage and/or how many peers we are + // already offering introducing?) + state.setSentRelayTag(_context.random().nextLong(MAX_TAG_VALUE)); + } else { + // don't offer to relay state.setSentRelayTag(0); + } if (_log.shouldLog(Log.DEBUG)) _log.debug("Send created to: " + state.getRemoteHostId().toString()); @@ -390,13 +420,89 @@ public class EstablishmentManager { private void sendRequest(OutboundEstablishState state) { long now = _context.clock().now(); - state.prepareSessionRequest(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Send request to: " + state.getRemoteHostId().toString()); _transport.send(_builder.buildSessionRequestPacket(state)); state.requestSent(); } + private static final long MAX_NONCE = 0xFFFFFFFFl; + /** if we don't get a relayResponse in 3 seconds, try again with another intro peer */ + private static final int INTRO_ATTEMPT_TIMEOUT = 3*1000; + + private void handlePendingIntro(OutboundEstablishState state) { + long nonce = _context.random().nextLong(MAX_NONCE); + while (true) { + synchronized (_liveIntroductions) { + OutboundEstablishState old = (OutboundEstablishState)_liveIntroductions.put(new Long(nonce), state); + if (old != null) { + nonce = _context.random().nextLong(MAX_NONCE); + } else { + break; + } + } + } + SimpleTimer.getInstance().addEvent(new FailIntroduction(state, nonce), INTRO_ATTEMPT_TIMEOUT); + state.setIntroNonce(nonce); + _context.statManager().addRateData("udp.sendIntroRelayRequest", 1, 0); + _transport.send(_builder.buildRelayRequest(state, _transport.getIntroKey())); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Send intro for " + state.getRemoteHostId().toString() + " with our intro key as " + _transport.getIntroKey().toBase64()); + state.introSent(); + } + private class FailIntroduction implements SimpleTimer.TimedEvent { + private long _nonce; + private OutboundEstablishState _state; + public FailIntroduction(OutboundEstablishState state, long nonce) { + _nonce = nonce; + _state = state; + } + public void timeReached() { + OutboundEstablishState removed = null; + synchronized (_liveIntroductions) { + removed = (OutboundEstablishState)_liveIntroductions.remove(new Long(_nonce)); + if (removed != _state) { + // another one with the same nonce in a very brief time... + _liveIntroductions.put(new Long(_nonce), removed); + removed = null; + } + } + if (removed != null) { + _context.statManager().addRateData("udp.sendIntroRelayTimeout", 1, 0); + notifyActivity(); + } + } + } + + public void receiveRelayResponse(RemoteHostId bob, UDPPacketReader reader) { + long nonce = reader.getRelayResponseReader().readNonce(); + OutboundEstablishState state = null; + synchronized (_liveIntroductions) { + state = (OutboundEstablishState)_liveIntroductions.remove(new Long(nonce)); + } + if (state == null) + return; // already established + + int sz = reader.getRelayResponseReader().readCharlieIPSize(); + byte ip[] = new byte[sz]; + reader.getRelayResponseReader().readCharlieIP(ip, 0); + InetAddress addr = null; + try { + addr = InetAddress.getByAddress(ip); + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Introducer for " + state + " (" + bob + ") sent us an invalid IP for our targer: " + Base64.encode(ip), uhe); + // these two cause this peer to requeue for a new intro peer + state.introductionFailed(); + notifyActivity(); + return; + } + _context.statManager().addRateData("udp.receiveIntroRelayResponse", state.getLifetime(), 0); + int port = reader.getRelayResponseReader().readCharliePort(); + state.introduced(addr, ip, port); + notifyActivity(); + } + private void sendConfirmation(OutboundEstablishState state) { long now = _context.clock().now(); boolean valid = state.validateSessionCreated(); @@ -594,6 +700,9 @@ public class EstablishmentManager { case OutboundEstablishState.STATE_REQUEST_SENT: err = "Took too long to establish remote connection (request sent)"; break; + case OutboundEstablishState.STATE_PENDING_INTRO: + err = "Took too long to establish remote connection (intro failed)"; + break; case OutboundEstablishState.STATE_UNKNOWN: // fallthrough default: err = "Took too long to establish remote connection (unknown state)"; @@ -626,6 +735,9 @@ public class EstablishmentManager { case OutboundEstablishState.STATE_CONFIRMED_COMPLETELY: handleCompletelyEstablished(outboundState); break; + case OutboundEstablishState.STATE_PENDING_INTRO: + handlePendingIntro(outboundState); + break; default: // wtf } diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java new file mode 100644 index 000000000..8968ddee9 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java @@ -0,0 +1,100 @@ +package net.i2p.router.transport.udp; + +import java.util.*; + +import net.i2p.data.SessionKey; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; + +/** + * + */ +public class IntroductionManager { + private RouterContext _context; + private Log _log; + private UDPTransport _transport; + private PacketBuilder _builder; + /** map of relay tag to PeerState that should receive the introduction */ + private Map _outbound; + /** list of peers (PeerState) who have given us introduction tags */ + private List _inbound; + + public IntroductionManager(RouterContext ctx, UDPTransport transport) { + _context = ctx; + _log = ctx.logManager().getLog(IntroductionManager.class); + _transport = transport; + _builder = new PacketBuilder(ctx); + _outbound = Collections.synchronizedMap(new HashMap(128)); + _inbound = new ArrayList(128); + } + + public void reset() { + _inbound.clear(); + _outbound.clear(); + } + + public void add(PeerState peer) { + if (peer == null) return; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Adding peer " + peer.getRemoteHostId() + ", weRelayToThemAs " + + peer.getWeRelayToThemAs() + ", theyRelayToUsAs " + peer.getTheyRelayToUsAs()); + if (peer.getWeRelayToThemAs() > 0) + _outbound.put(new Long(peer.getWeRelayToThemAs()), peer); + if (peer.getTheyRelayToUsAs() > 0) { + synchronized (_inbound) { + if (!_inbound.contains(peer)) + _inbound.add(peer); + } + } + } + + public void remove(PeerState peer) { + if (peer == null) return; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("removing peer " + peer.getRemoteHostId() + ", weRelayToThemAs " + + peer.getWeRelayToThemAs() + ", theyRelayToUsAs " + peer.getTheyRelayToUsAs()); + if (peer.getWeRelayToThemAs() > 0) + _outbound.remove(new Long(peer.getWeRelayToThemAs())); + if (peer.getTheyRelayToUsAs() > 0) { + synchronized (_inbound) { + _inbound.remove(peer); + } + } + } + + public PeerState get(long id) { + return (PeerState)_outbound.get(new Long(id)); + } + + public void pickInbound(List rv, int howMany) { + int start = _context.random().nextInt(Integer.MAX_VALUE); + synchronized (_inbound) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Picking inbound out of " + _inbound); + if (_inbound.size() <= 0) return; + start = start % _inbound.size(); + for (int i = 0; i < _inbound.size() && rv.size() < howMany; i++) { + PeerState cur = (PeerState)_inbound.get((start + i) % _inbound.size()); + rv.add(cur); + } + } + } + + public void receiveRelayIntro(RemoteHostId bob, UDPPacketReader reader) { + _transport.send(_builder.buildHolePunch(reader)); + } + + public void receiveRelayRequest(RemoteHostId alice, UDPPacketReader reader) { + long tag = reader.getRelayRequestReader().readTag(); + PeerState charlie = _transport.getPeerState(tag); + if (charlie == null) + return; + byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; + reader.getRelayRequestReader().readAliceIntroKey(key, 0); + SessionKey aliceIntroKey = new SessionKey(key); + // send that peer an introduction for alice + _transport.send(_builder.buildRelayIntro(alice, charlie, reader.getRelayRequestReader())); + // send alice back charlie's info + _transport.send(_builder.buildRelayResponse(alice, charlie, reader.getRelayRequestReader().readNonce(), aliceIntroKey)); + } +} diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java index b64b79617..d80e2a3d6 100644 --- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java @@ -56,6 +56,9 @@ public class OutboundEstablishState { private SessionKey _introKey; private List _queuedMessages; private int _currentState; + private long _introductionNonce; + // intro + private UDPAddress _remoteAddress; /** nothin sent yet */ public static final int STATE_UNKNOWN = 0; @@ -67,12 +70,14 @@ public class OutboundEstablishState { public static final int STATE_CONFIRMED_PARTIALLY = 3; /** we have received a data packet */ public static final int STATE_CONFIRMED_COMPLETELY = 4; + /** we need to have someone introduce us to the peer, but haven't received a RelayResponse yet */ + public static final int STATE_PENDING_INTRO = 5; public OutboundEstablishState(RouterContext ctx, InetAddress remoteHost, int remotePort, - RouterIdentity remotePeer, SessionKey introKey) { + RouterIdentity remotePeer, SessionKey introKey, UDPAddress addr) { _context = ctx; _log = ctx.logManager().getLog(OutboundEstablishState.class); - _bobIP = remoteHost.getAddress(); + _bobIP = (remoteHost != null ? remoteHost.getAddress() : null); _bobPort = remotePort; _remoteHostId = new RemoteHostId(_bobIP, _bobPort); _remotePeer = remotePeer; @@ -81,10 +86,22 @@ public class OutboundEstablishState { _queuedMessages = new ArrayList(4); _currentState = STATE_UNKNOWN; _establishBegin = ctx.clock().now(); + _remoteAddress = addr; + _introductionNonce = -1; + prepareSessionRequest(); + if ( (addr != null) && (addr.getIntroducerCount() > 0) ) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("new outbound establish to " + remotePeer.calculateHash().toBase64() + ", with address: " + addr); + _currentState = STATE_PENDING_INTRO; + } } public synchronized int getState() { return _currentState; } + public UDPAddress getRemoteAddress() { return _remoteAddress; } + public void setIntroNonce(long nonce) { _introductionNonce = nonce; } + public long getIntroNonce() { return _introductionNonce; } + public void addMessage(OutNetMessage msg) { synchronized (_queuedMessages) { _queuedMessages.add(msg); @@ -101,7 +118,7 @@ public class OutboundEstablishState { public RouterIdentity getRemoteIdentity() { return _remotePeer; } public SessionKey getIntroKey() { return _introKey; } - public synchronized void prepareSessionRequest() { + private void prepareSessionRequest() { _keyBuilder = new DHSessionKeyBuilder(); byte X[] = _keyBuilder.getMyPublicValue().toByteArray(); if (_sentX == null) @@ -342,6 +359,31 @@ public class OutboundEstablishState { if (_currentState == STATE_UNKNOWN) _currentState = STATE_REQUEST_SENT; } + public synchronized void introSent() { + _lastSend = _context.clock().now(); + _nextSend = _lastSend + 5*1000; + if (_currentState == STATE_UNKNOWN) + _currentState = STATE_PENDING_INTRO; + } + public synchronized void introductionFailed() { + _nextSend = _context.clock().now(); + // keep the state as STATE_PENDING_INTRO, so next time the EstablishmentManager asks us + // whats up, it'll try a new random intro peer + } + + public synchronized void introduced(InetAddress bob, byte bobIP[], int bobPort) { + if (_currentState != STATE_PENDING_INTRO) + return; // we've already successfully been introduced, so don't overwrite old settings + _nextSend = _context.clock().now() + 500; // wait briefly for the hole punching + if (_currentState == STATE_PENDING_INTRO) { + // STATE_UNKNOWN will probe the EstablishmentManager to send a new + // session request to this newly known address + _currentState = STATE_UNKNOWN; + } + _bobIP = bobIP; + _bobPort = bobPort; + _remoteHostId = new RemoteHostId(bobIP, bobPort); + } /** how long have we been trying to establish this session? */ public synchronized long getLifetime() { return _context.clock().now() - _establishBegin; } diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java index f0bf1ac0e..3eedc3b80 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java @@ -628,6 +628,228 @@ public class PacketBuilder { return packet; } + /** + * full flag info for a relay request message. this can be fixed, + * since we never rekey on relay request, and don't need any extended options + */ + private static final byte PEER_RELAY_REQUEST_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_RELAY_REQUEST << 4); + + // specify these if we know what our external receive ip/port is and if its different + // from what bob is going to think + private byte[] getOurExplicitIP() { return null; } + private int getOurExplicitPort() { return 0; } + + public UDPPacket buildRelayRequest(OutboundEstablishState state, SessionKey ourIntroKey) { + UDPAddress addr = state.getRemoteAddress(); + int index = _context.random().nextInt(UDPAddress.MAX_INTRODUCERS) % addr.getIntroducerCount(); + InetAddress iaddr = addr.getIntroducerHost(index); + int iport = addr.getIntroducerPort(index); + byte ikey[] = addr.getIntroducerKey(index); + long tag = addr.getIntroducerTag(index); + return buildRelayRequest(iaddr, iport, ikey, tag, ourIntroKey, state.getIntroNonce(), true); + } + + public UDPPacket buildRelayRequest(InetAddress introHost, int introPort, byte introKey[], long introTag, SessionKey ourIntroKey, long introNonce, boolean encrypt) { + UDPPacket packet = UDPPacket.acquire(_context); + byte data[] = packet.getPacket().getData(); + Arrays.fill(data, 0, data.length, (byte)0x0); + int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; + + byte ourIP[] = getOurExplicitIP(); + int ourPort = getOurExplicitPort(); + + // header + data[off] = PEER_RELAY_REQUEST_FLAG_BYTE; + off++; + long now = _context.clock().now() / 1000; + DataHelper.toLong(data, off, 4, now); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending intro relay request to " + introHost + ":" + introPort); // + " regarding " + state.getRemoteIdentity().calculateHash().toBase64()); + off += 4; + + // now for the body + DataHelper.toLong(data, off, 4, introTag); + off += 4; + if (ourIP != null) { + DataHelper.toLong(data, off, 1, ourIP.length); + off++; + System.arraycopy(ourIP, 0, data, off, ourIP.length); + off += ourIP.length; + } else { + DataHelper.toLong(data, off, 1, 0); + off++; + } + + DataHelper.toLong(data, off, 2, ourPort); + off += 2; + + // challenge... + DataHelper.toLong(data, off, 1, 0); + off++; + off += 0; // *cough* + + System.arraycopy(ourIntroKey.getData(), 0, data, off, SessionKey.KEYSIZE_BYTES); + off += SessionKey.KEYSIZE_BYTES; + + if (_log.shouldLog(Log.WARN)) + _log.warn("wrote alice intro key: " + Base64.encode(data, off-SessionKey.KEYSIZE_BYTES, SessionKey.KEYSIZE_BYTES) + + " with nonce " + introNonce + " size=" + (off+4 + (16 - (off+4)%16)) + + " and data: " + Base64.encode(data, 0, off)); + + DataHelper.toLong(data, off, 4, introNonce); + off += 4; + + // we can pad here if we want, maybe randomized? + + // pad up so we're on the encryption boundary + if ( (off % 16) != 0) + off += 16 - (off % 16); + packet.getPacket().setLength(off); + if (encrypt) + authenticate(packet, new SessionKey(introKey), new SessionKey(introKey)); + setTo(packet, introHost, introPort); + return packet; + } + + /** + * full flag info for a relay intro message. this can be fixed, + * since we never rekey on relay request, and don't need any extended options + */ + private static final byte PEER_RELAY_INTRO_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_RELAY_INTRO << 4); + + public UDPPacket buildRelayIntro(RemoteHostId alice, PeerState charlie, UDPPacketReader.RelayRequestReader request) { + UDPPacket packet = UDPPacket.acquire(_context); + byte data[] = packet.getPacket().getData(); + Arrays.fill(data, 0, data.length, (byte)0x0); + int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; + + // header + data[off] = PEER_RELAY_INTRO_FLAG_BYTE; + off++; + long now = _context.clock().now() / 1000; + DataHelper.toLong(data, off, 4, now); + if (_log.shouldLog(Log.INFO)) + _log.info("Sending intro to " + charlie + " for " + alice); + off += 4; + + // now for the body + byte ip[] = alice.getIP(); + DataHelper.toLong(data, off, 1, ip.length); + off++; + System.arraycopy(ip, 0, data, off, ip.length); + off += ip.length; + DataHelper.toLong(data, off, 2, alice.getPort()); + off += 2; + + int sz = request.readChallengeSize(); + DataHelper.toLong(data, off, 1, sz); + off++; + if (sz > 0) { + request.readChallengeSize(data, off); + off += sz; + } + + // we can pad here if we want, maybe randomized? + + // pad up so we're on the encryption boundary + if ( (off % 16) != 0) + off += 16 - (off % 16); + packet.getPacket().setLength(off); + authenticate(packet, charlie.getCurrentCipherKey(), charlie.getCurrentMACKey()); + setTo(packet, charlie.getRemoteIPAddress(), charlie.getRemotePort()); + return packet; + } + + /** + * full flag info for a relay response message. this can be fixed, + * since we never rekey on relay response, and don't need any extended options + */ + private static final byte PEER_RELAY_RESPONSE_FLAG_BYTE = (UDPPacket.PAYLOAD_TYPE_RELAY_RESPONSE << 4); + + public UDPPacket buildRelayResponse(RemoteHostId alice, PeerState charlie, long nonce, SessionKey aliceIntroKey) { + InetAddress aliceAddr = null; + try { + aliceAddr = InetAddress.getByAddress(alice.getIP()); + } catch (UnknownHostException uhe) { + return null; + } + + UDPPacket packet = UDPPacket.acquire(_context); + byte data[] = packet.getPacket().getData(); + Arrays.fill(data, 0, data.length, (byte)0x0); + int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; + + // header + data[off] = PEER_RELAY_RESPONSE_FLAG_BYTE; + off++; + long now = _context.clock().now() / 1000; + DataHelper.toLong(data, off, 4, now); + off += 4; + + if (_log.shouldLog(Log.INFO)) + _log.info("Sending relay response to " + alice + " for " + charlie + " with alice's intro key " + aliceIntroKey.toBase64()); + + // now for the body + byte charlieIP[] = charlie.getRemoteIP(); + DataHelper.toLong(data, off, 1, charlieIP.length); + off++; + System.arraycopy(charlieIP, 0, data, off, charlieIP.length); + off += charlieIP.length; + DataHelper.toLong(data, off, 2, charlie.getRemotePort()); + off += 2; + + byte aliceIP[] = alice.getIP(); + DataHelper.toLong(data, off, 1, aliceIP.length); + off++; + System.arraycopy(aliceIP, 0, data, off, aliceIP.length); + off += aliceIP.length; + DataHelper.toLong(data, off, 2, alice.getPort()); + off += 2; + + DataHelper.toLong(data, off, 4, nonce); + off += 4; + + // we can pad here if we want, maybe randomized? + + // pad up so we're on the encryption boundary + if ( (off % 16) != 0) + off += 16 - (off % 16); + packet.getPacket().setLength(off); + authenticate(packet, aliceIntroKey, aliceIntroKey); + setTo(packet, aliceAddr, alice.getPort()); + return packet; + } + + public UDPPacket buildHolePunch(UDPPacketReader reader) { + UDPPacket packet = UDPPacket.acquire(_context); + byte data[] = packet.getPacket().getData(); + Arrays.fill(data, 0, data.length, (byte)0x0); + int off = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE; + + int ipSize = reader.getRelayIntroReader().readIPSize(); + byte ip[] = new byte[ipSize]; + reader.getRelayIntroReader().readIP(ip, 0); + int port = reader.getRelayIntroReader().readPort(); + + InetAddress to = null; + try { + to = InetAddress.getByAddress(ip); + } catch (UnknownHostException uhe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("IP for alice to hole punch to is invalid", uhe); + return null; + } + + if (_log.shouldLog(Log.INFO)) + _log.info("Sending relay hole punch to " + to + ":" + port); + + // the packet is empty and does not need to be authenticated, since + // its just for hole punching + packet.getPacket().setLength(0); + setTo(packet, to, port); + return packet; + } + private void setTo(UDPPacket packet, InetAddress ip, int port) { packet.getPacket().setAddress(ip); packet.getPacket().setPort(port); diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java index 19c891ab0..35caf3192 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -30,6 +30,7 @@ public class PacketHandler { private EstablishmentManager _establisher; private InboundMessageFragments _inbound; private PeerTestManager _testManager; + private IntroductionManager _introManager; private boolean _keepReading; private List _handlers; @@ -38,7 +39,7 @@ public class PacketHandler { private static final long GRACE_PERIOD = Router.CLOCK_FUDGE_FACTOR + 30*1000; - public PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound, PeerTestManager testManager) { + public PacketHandler(RouterContext ctx, UDPTransport transport, UDPEndpoint endpoint, EstablishmentManager establisher, InboundMessageFragments inbound, PeerTestManager testManager, IntroductionManager introManager) { _context = ctx; _log = ctx.logManager().getLog(PacketHandler.class); _transport = transport; @@ -46,6 +47,7 @@ public class PacketHandler { _establisher = establisher; _inbound = inbound; _testManager = testManager; + _introManager = introManager; _handlers = new ArrayList(NUM_HANDLERS); for (int i = 0; i < NUM_HANDLERS; i++) { _handlers.add(new Handler()); @@ -193,8 +195,8 @@ public class PacketHandler { // process, so try our intro key // (after an outbound establishment process, there wouldn't // be any stray packets) - if (_log.shouldLog(Log.INFO)) - _log.info("Validation with existing con failed, but validation as reestablish/stray passed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Validation with existing con failed, but validation as reestablish/stray passed"); packet.decrypt(_transport.getIntroKey()); } else { _state = 21; @@ -235,8 +237,8 @@ public class PacketHandler { _state = 28; return; } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Valid introduction packet received: " + packet); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Valid introduction packet received: " + packet); } _state = 29; @@ -392,16 +394,31 @@ public class PacketHandler { _state = 50; if (outState != null) state = _establisher.receiveData(outState); - if (_log.shouldLog(Log.INFO)) - _log.info("Received new DATA packet from " + state + ": " + packet); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received new DATA packet from " + state + ": " + packet); _inbound.receiveData(state, reader.getDataReader()); break; case UDPPacket.PAYLOAD_TYPE_TEST: _state = 51; - if (_log.shouldLog(Log.INFO)) - _log.info("Received test packet: " + reader + " from " + from); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received test packet: " + reader + " from " + from); _testManager.receiveTest(from, reader); break; + case UDPPacket.PAYLOAD_TYPE_RELAY_REQUEST: + if (_log.shouldLog(Log.INFO)) + _log.info("Received relay request packet: " + reader + " from " + from); + _introManager.receiveRelayRequest(from, reader); + break; + case UDPPacket.PAYLOAD_TYPE_RELAY_INTRO: + if (_log.shouldLog(Log.INFO)) + _log.info("Received relay intro packet: " + reader + " from " + from); + _introManager.receiveRelayIntro(from, reader); + break; + case UDPPacket.PAYLOAD_TYPE_RELAY_RESPONSE: + if (_log.shouldLog(Log.INFO)) + _log.info("Received relay response packet: " + reader + " from " + from); + _establisher.receiveRelayResponse(from, reader); + break; default: _state = 52; if (_log.shouldLog(Log.WARN)) diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java index b3ea7dac9..62ab59f66 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java @@ -125,6 +125,12 @@ class PeerTestManager { } } + /** + * If we have sent a packet to charlie within the last 3 minutes, ignore any test + * results we get from them, as our NAT will have poked a hole anyway + * + */ + private static final long CHARLIE_RECENT_PERIOD = 3*60*1000; /** * Receive a PeerTest message which contains the correct nonce for our current @@ -152,7 +158,10 @@ class PeerTestManager { } } else { PeerState charlieSession = _transport.getPeerState(from); - if (charlieSession != null) { + long recentBegin = _context.clock().now() - CHARLIE_RECENT_PERIOD; + if ( (charlieSession != null) && + (charlieSession.getLastACKSend() > recentBegin) && + (charlieSession.getLastSendTime() > recentBegin) ) { if (_log.shouldLog(Log.WARN)) _log.warn("Bob chose a charlie we already have a session to, cancelling the test and rerunning (bob: " + _currentTest + ", charlie: " + from + ")"); diff --git a/router/java/src/net/i2p/router/transport/udp/RelayPeer.java b/router/java/src/net/i2p/router/transport/udp/RelayPeer.java deleted file mode 100644 index 6b42d0e2e..000000000 --- a/router/java/src/net/i2p/router/transport/udp/RelayPeer.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.i2p.router.transport.udp; - -import net.i2p.data.SessionKey; - -/** - * Describe the offering to act as an introducer - * - */ -class RelayPeer { - private String _host; - private int _port; - private byte _tag[]; - private SessionKey _relayIntroKey; - public RelayPeer(String host, int port, byte tag[], SessionKey introKey) { - _host = host; - _port = port; - _tag = tag; - _relayIntroKey = introKey; - } - public String getHost() { return _host; } - public int getPort() { return _port; } - public byte[] getTag() { return _tag; } - public SessionKey getIntroKey() { return _relayIntroKey; } -} diff --git a/router/java/src/net/i2p/router/transport/udp/RemoteHostId.java b/router/java/src/net/i2p/router/transport/udp/RemoteHostId.java index af3813c67..2e7b69dfb 100644 --- a/router/java/src/net/i2p/router/transport/udp/RemoteHostId.java +++ b/router/java/src/net/i2p/router/transport/udp/RemoteHostId.java @@ -36,11 +36,17 @@ final class RemoteHostId { return (_port == id.getPort()) && DataHelper.eq(_ip, id.getIP()); } - public String toString() { + public String toString() { return toString(true); } + public String toString(boolean includePort) { StringBuffer buf = new StringBuffer(_ip.length + 5); - for (int i = 0; i < _ip.length; i++) - buf.append(_ip[i]&0xFF).append('.'); - buf.append(_port); + for (int i = 0; i < _ip.length; i++) { + buf.append(_ip[i]&0xFF); + if (i + 1 < _ip.length) + buf.append('.'); + } + if (includePort) + buf.append(':').append(_port); return buf.toString(); } + public String toHostString() { return toString(false); } } diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java index a9961948c..9431b368f 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java @@ -6,6 +6,7 @@ import java.util.Properties; import net.i2p.data.Base64; import net.i2p.data.RouterAddress; +import net.i2p.data.SessionKey; /** * basic helper to parse out peer info from a udp address @@ -15,6 +16,11 @@ public class UDPAddress { private InetAddress _hostAddress; private int _port; private byte[] _introKey; + private String _introHosts[]; + private InetAddress _introAddresses[]; + private int _introPorts[]; + private byte[] _introKeys[]; + private long _introTags[]; public static final String PROP_PORT = "port"; public static final String PROP_HOST = "host"; @@ -23,11 +29,36 @@ public class UDPAddress { public static final String PROP_CAPACITY = "caps"; public static final char CAPACITY_TESTING = 'B'; public static final char CAPACITY_INTRODUCER = 'C'; + + public static final String PROP_INTRO_HOST_PREFIX = "ihost"; + public static final String PROP_INTRO_PORT_PREFIX = "iport"; + public static final String PROP_INTRO_KEY_PREFIX = "ikey"; + public static final String PROP_INTRO_TAG_PREFIX = "itag"; + static final int MAX_INTRODUCERS = 3; public UDPAddress(RouterAddress addr) { parse(addr); } + public String toString() { + StringBuffer rv = new StringBuffer(64); + rv.append("[SSU "); + if (_host != null) + rv.append("host: ").append(_host).append(' '); + if (_port > 0) + rv.append("port: ").append(_port).append(' '); + if (_introKey != null) + rv.append("key: ").append(Base64.encode(_introKey)).append(' '); + if (_introHosts != null) { + for (int i = 0; i < _introHosts.length; i++) { + rv.append("intro[" + i + "]: ").append(_introHosts[i]); + rv.append(':').append(_introPorts[i]); + rv.append('/').append(Base64.encode(_introKeys[i])).append(' '); + } + } + return rv.toString(); + } + private void parse(RouterAddress addr) { Properties opts = addr.getOptions(); _host = opts.getProperty(PROP_HOST); @@ -42,6 +73,45 @@ public class UDPAddress { String key = opts.getProperty(PROP_INTRO_KEY); if (key != null) _introKey = Base64.decode(key.trim()); + + for (int i = MAX_INTRODUCERS; i >= 0; i--) { + String host = opts.getProperty(PROP_INTRO_HOST_PREFIX + i); + if (host == null) continue; + String port = opts.getProperty(PROP_INTRO_PORT_PREFIX + i); + if (port == null) continue; + String k = opts.getProperty(PROP_INTRO_KEY_PREFIX + i); + if (k == null) continue; + byte ikey[] = Base64.decode(k); + if ( (ikey == null) || (ikey.length != SessionKey.KEYSIZE_BYTES) ) + continue; + String t = opts.getProperty(PROP_INTRO_TAG_PREFIX + i); + if (t == null) continue; + int p = -1; + try { + p = Integer.parseInt(port); + if (p <= 0) continue; + } catch (NumberFormatException nfe) { + continue; + } + long tag = -1; + try { + tag = Long.parseLong(t); + if (tag <= 0) continue; + } catch (NumberFormatException nfe) { + continue; + } + if (_introHosts == null) { + _introHosts = new String[i+1]; + _introPorts = new int[i+1]; + _introAddresses = new InetAddress[i+1]; + _introKeys = new byte[i+1][]; + _introTags = new long[i+1]; + } + _introHosts[i] = host; + _introPorts[i] = p; + _introKeys[i] = ikey; + _introTags[i] = tag; + } } public String getHost() { return _host; } @@ -57,4 +127,20 @@ public class UDPAddress { } public int getPort() { return _port; } public byte[] getIntroKey() { return _introKey; } + + public int getIntroducerCount() { return (_introAddresses == null ? 0 : _introAddresses.length); } + public InetAddress getIntroducerHost(int i) { + if (_introAddresses[i] == null) { + try { + _introAddresses[i] = InetAddress.getByName(_introHosts[i]); + } catch (UnknownHostException uhe) { + _introAddresses[i] = null; + } + } + return _introAddresses[i]; + } + public int getIntroducerPort(int i) { return _introPorts[i]; } + public byte[] getIntroducerKey(int i) { return _introKeys[i]; } + public long getIntroducerTag(int i) { return _introTags[i]; } + } diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java index 4a22875ba..d473a5f05 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java @@ -1,5 +1,6 @@ package net.i2p.router.transport.udp; +import java.net.InetAddress; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataHelper; @@ -24,6 +25,9 @@ public class UDPPacketReader { private SessionConfirmedReader _sessionConfirmedReader; private DataReader _dataReader; private PeerTestReader _peerTestReader; + private RelayRequestReader _relayRequestReader; + private RelayIntroReader _relayIntroReader; + private RelayResponseReader _relayResponseReader; private static final int KEYING_MATERIAL_LENGTH = 64; @@ -35,6 +39,9 @@ public class UDPPacketReader { _sessionConfirmedReader = new SessionConfirmedReader(); _dataReader = new DataReader(); _peerTestReader = new PeerTestReader(); + _relayRequestReader = new RelayRequestReader(); + _relayIntroReader = new RelayIntroReader(); + _relayResponseReader = new RelayResponseReader(); } public void initialize(UDPPacket packet) { @@ -93,6 +100,9 @@ public class UDPPacketReader { public SessionConfirmedReader getSessionConfirmedReader() { return _sessionConfirmedReader; } public DataReader getDataReader() { return _dataReader; } public PeerTestReader getPeerTestReader() { return _peerTestReader; } + public RelayRequestReader getRelayRequestReader() { return _relayRequestReader; } + public RelayIntroReader getRelayIntroReader() { return _relayIntroReader; } + public RelayResponseReader getRelayResponseReader() { return _relayResponseReader; } public String toString() { switch (readPayloadType()) { @@ -106,6 +116,12 @@ public class UDPPacketReader { return "Session request packet"; case UDPPacket.PAYLOAD_TYPE_TEST: return "Peer test packet"; + case UDPPacket.PAYLOAD_TYPE_RELAY_INTRO: + return "Relay intro packet"; + case UDPPacket.PAYLOAD_TYPE_RELAY_REQUEST: + return "Relay request packet"; + case UDPPacket.PAYLOAD_TYPE_RELAY_RESPONSE: + return "Relay response packet"; default: return "Other packet type..."; } @@ -538,4 +554,214 @@ public class UDPPacketReader { System.arraycopy(_message, offset, target, targetOffset, SessionKey.KEYSIZE_BYTES); } } + + /** Help read the RelayRequest payload */ + public class RelayRequestReader { + public long readTag() { + long rv = DataHelper.fromLong(_message, readBodyOffset(), 4); + if (_log.shouldLog(Log.WARN)) + _log.warn("read alice tag: " + rv); + return rv; + } + public int readIPSize() { + int offset = readBodyOffset() + 4; + int rv = (int)DataHelper.fromLong(_message, offset, 1); + if (_log.shouldLog(Log.WARN)) + _log.warn("read alice ip size: " + rv); + return rv; + } + + /** what IP Alice is reachable on */ + public void readIP(byte target[], int targetOffset) { + int offset = readBodyOffset() + 4; + int size = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, size); + if (_log.shouldLog(Log.WARN)) + _log.warn("read alice ip: " + Base64.encode(target, targetOffset, size)); + } + public int readPort() { + int offset = readBodyOffset() + 4; + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + int rv = (int)DataHelper.fromLong(_message, offset, 2); + if (_log.shouldLog(Log.WARN)) + _log.warn("read alice port: " + rv); + return rv; + } + public int readChallengeSize() { + int offset = readBodyOffset() + 4; + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int rv = (int)DataHelper.fromLong(_message, offset, 1); + if (_log.shouldLog(Log.WARN)) + _log.warn("read challenge size: " + rv); + return rv; + } + public void readChallengeSize(byte target[], int targetOffset) { + int offset = readBodyOffset() + 4; + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, sz); + if (_log.shouldLog(Log.WARN)) + _log.warn("read challenge data: " + Base64.encode(target)); + } + public void readAliceIntroKey(byte target[], int targetOffset) { + int offset = readBodyOffset() + 4; + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + offset += sz; + System.arraycopy(_message, offset, target, targetOffset, SessionKey.KEYSIZE_BYTES); + if (_log.shouldLog(Log.WARN)) + _log.warn("read alice intro key: " + Base64.encode(target, targetOffset, SessionKey.KEYSIZE_BYTES) + + " packet size: " + _payloadLength + " off: " + offset + " data: " + Base64.encode(_message)); + } + public long readNonce() { + int offset = readBodyOffset() + 4; + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + offset += sz; + offset += SessionKey.KEYSIZE_BYTES; + long rv = DataHelper.fromLong(_message, offset, 4); + if (_log.shouldLog(Log.WARN)) + _log.warn("read request nonce: " + rv); + return rv; + } + } + + /** Help read the RelayIntro payload */ + public class RelayIntroReader { + public int readIPSize() { + int offset = readBodyOffset(); + return (int)DataHelper.fromLong(_message, offset, 1); + } + + /** what IP Alice is reachable on */ + public void readIP(byte target[], int targetOffset) { + int offset = readBodyOffset(); + int size = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, size); + } + public int readPort() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + return (int)DataHelper.fromLong(_message, offset, 2); + } + public int readChallengeSize() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + return (int)DataHelper.fromLong(_message, offset, 1); + } + public void readChallengeSize(byte target[], int targetOffset) { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, sz); + } + } + + + /** Help read the RelayResponse payload */ + public class RelayResponseReader { + public int readCharlieIPSize() { + int offset = readBodyOffset(); + return (int)DataHelper.fromLong(_message, offset, 1); + } + /** what IP charlie is reachable on */ + public void readCharlieIP(byte target[], int targetOffset) { + int offset = readBodyOffset(); + int size = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, size); + } + /** what port charlie is reachable on */ + public int readCharliePort() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + return (int)DataHelper.fromLong(_message, offset, 2); + } + + public int readAliceIPSize() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + return (int)DataHelper.fromLong(_message, offset, 1); + } + public void readAliceIP(byte target[], int targetOffset) { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + System.arraycopy(_message, offset, target, targetOffset, sz); + } + public int readAlicePort() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + offset += sz; + return (int)DataHelper.fromLong(_message, offset, 2); + } + public long readNonce() { + int offset = readBodyOffset(); + offset += DataHelper.fromLong(_message, offset, 1); + offset++; + offset += 2; + int sz = (int)DataHelper.fromLong(_message, offset, 1); + offset++; + offset += sz; + offset += 2; + return DataHelper.fromLong(_message, offset, 4); + } + } + + + public static void main(String args[]) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + try { + PacketBuilder b = new PacketBuilder(ctx); + InetAddress introHost = InetAddress.getLocalHost(); + int introPort = 1234; + byte introKey[] = new byte[SessionKey.KEYSIZE_BYTES]; + ctx.random().nextBytes(introKey); + long introTag = ctx.random().nextLong(0xFFFFFFFFl); + long introNonce = ctx.random().nextLong(0xFFFFFFFFl); + SessionKey ourIntroKey = ctx.keyGenerator().generateSessionKey(); + UDPPacket packet = b.buildRelayRequest(introHost, introPort, introKey, introTag, ourIntroKey, introNonce, false); + UDPPacketReader r = new UDPPacketReader(ctx); + r.initialize(packet); + RelayRequestReader reader = r.getRelayRequestReader(); + System.out.println("Nonce: " + reader.readNonce() + " / " + introNonce); + System.out.println("Tag : " + reader.readTag() + " / " + introTag); + byte readKey[] = new byte[SessionKey.KEYSIZE_BYTES]; + reader.readAliceIntroKey(readKey, 0); + System.out.println("Key : " + Base64.encode(readKey) + " / " + ourIntroKey.toBase64()); + } catch (Exception e) { + e.printStackTrace(); + } + + } } diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java index c0dd5e4f6..8d0ada03e 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java @@ -42,6 +42,7 @@ public class UDPReceiver { _context.statManager().createRateStat("udp.droppedInbound", "How many packet are queued up but not yet received when we drop", "udp", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("udp.droppedInboundProbabalistically", "How many packet we drop probabalistically (to simulate failures)", "udp", new long[] { 60*1000, 5*60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("udp.acceptedInboundProbabalistically", "How many packet we accept probabalistically (to simulate failures)", "udp", new long[] { 60*1000, 5*60*1000, 10*60*1000, 60*60*1000 }); + _context.statManager().createRateStat("udp.receiveHolePunch", "How often we receive a NAT hole punch", "udp", new long[] { 60*1000, 5*60*1000, 10*60*1000, 60*60*1000 }); } public void startup() { @@ -212,10 +213,15 @@ public class UDPReceiver { FIFOBandwidthLimiter.Request req = _context.bandwidthLimiter().requestInbound(size, "UDP receiver"); while (req.getPendingInboundRequested() > 0) req.waitForNextAllocation(); + + int queued = receive(packet); + _context.statManager().addRateData("udp.receivePacketSize", size, queued); + } else { + _context.statManager().addRateData("udp.receiveHolePunch", 1, 0); + // nat hole punch packets are 0 bytes + if (_log.shouldLog(Log.INFO)) + _log.info("Received a 0 byte udp packet from " + packet.getPacket().getAddress() + ":" + packet.getPacket().getPort()); } - - int queued = receive(packet); - _context.statManager().addRateData("udp.receivePacketSize", size, queued); } catch (IOException ioe) { if (_socketChanged) { if (_log.shouldLog(Log.INFO)) diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java index 6a13bb095..749622317 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java @@ -40,8 +40,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority private Map _peersByIdent; /** RemoteHostId to PeerState */ private Map _peersByRemoteHost; - /** Relay tag (base64 String) to PeerState */ - private Map _peersByRelayTag; /** * Array of list of PeerState instances, where each list contains peers with one * of the given capacities (from 0-25, referencing 'A'-'Z'). @@ -57,14 +55,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority private InboundMessageFragments _inboundFragments; private UDPFlooder _flooder; private PeerTestManager _testManager; + private IntroductionManager _introManager; private ExpirePeerEvent _expireEvent; private PeerTestEvent _testEvent; private short _reachabilityStatus; private long _reachabilityStatusLastUpdated; + private long _introducersSelectedOn; - /** list of RelayPeer objects for people who will relay to us */ - private List _relayPeers; - /** summary info to distribute */ private RouterAddress _externalAddress; /** port number on which we can be reached, or -1 */ @@ -122,7 +119,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _log = ctx.logManager().getLog(UDPTransport.class); _peersByIdent = new HashMap(128); _peersByRemoteHost = new HashMap(128); - _peersByRelayTag = new HashMap(128); _peersByCapacity = new ArrayList['Z'-'A'+1]; for (int i = 0; i < _peersByCapacity.length; i++) _peersByCapacity[i] = new ArrayList(16); @@ -131,7 +127,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority TimedWeightedPriorityMessageQueue mq = new TimedWeightedPriorityMessageQueue(ctx, PRIORITY_LIMITS, PRIORITY_WEIGHT, this); _outboundMessages = mq; _activeThrottle = mq; - _relayPeers = new ArrayList(1); _fastBid = new SharedBid(50); _slowBid = new SharedBid(1000); @@ -143,6 +138,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _expireEvent = new ExpirePeerEvent(); _testEvent = new PeerTestEvent(); _reachabilityStatus = CommSystemFacade.STATUS_UNKNOWN; + _introManager = new IntroductionManager(_context, this); + _introducersSelectedOn = -1; _context.statManager().createRateStat("udp.droppedPeer", "How long ago did we receive from a dropped peer (duration == session lifetime", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); _context.statManager().createRateStat("udp.droppedPeerInactive", "How long ago did we receive from a dropped peer (duration == session lifetime)", "udp", new long[] { 60*60*1000, 24*60*60*1000 }); @@ -172,6 +169,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _inboundFragments.shutdown(); if (_flooder != null) _flooder.shutdown(); + _introManager.reset(); _introKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]); System.arraycopy(_context.routerHash().getData(), 0, _introKey.getData(), 0, SessionKey.KEYSIZE_BYTES); @@ -225,7 +223,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _testManager = new PeerTestManager(_context, this); if (_handler == null) - _handler = new PacketHandler(_context, this, _endpoint, _establisher, _inboundFragments, _testManager); + _handler = new PacketHandler(_context, this, _endpoint, _establisher, _inboundFragments, _testManager, _introManager); if (_refiller == null) _refiller = new OutboundRefiller(_context, _fragments, _outboundMessages); @@ -365,10 +363,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority * get the state for the peer being introduced, or null if we aren't * offering to introduce anyone with that tag. */ - public PeerState getPeerState(String relayTag) { - synchronized (_peersByRelayTag) { - return (PeerState)_peersByRelayTag.get(relayTag); - } + public PeerState getPeerState(long relayTag) { + return _introManager.get(relayTag); } /** @@ -486,8 +482,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } } - if ( (oldPeer != null) && (_log.shouldLog(Log.WARN)) ) - _log.warn("Peer already connected: old=" + oldPeer + " new=" + peer, new Exception("dup")); oldPeer = null; RemoteHostId remoteId = peer.getRemoteHostId(); @@ -513,11 +507,24 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _expireEvent.add(peer); + _introManager.add(peer); + if (oldEstablishedOn > 0) _context.statManager().addRateData("udp.alreadyConnected", oldEstablishedOn, 0); + + // if we need introducers, try to shift 'em around every 10 minutes + if (introducersRequired() && (_introducersSelectedOn < _context.clock().now() - 10*60*1000)) + rebuildExternalAddress(); return true; } + public RouterAddress getCurrentAddress() { + // if we need introducers, try to shift 'em around every 10 minutes + if (introducersRequired() && (_introducersSelectedOn < _context.clock().now() - 10*60*1000)) + rebuildExternalAddress(false); + return super.getCurrentAddress(); + } + private void dropPeer(PeerState peer) { dropPeer(peer, true); } @@ -562,6 +569,11 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _log.info(buf.toString(), new Exception("Dropped by")); } + _introManager.remove(peer); + // a bit overzealous - perhaps we should only rebuild the external if the peer being dropped + // is one of our introducers? + rebuildExternalAddress(); + if (peer.getRemotePeer() != null) { dropPeerCapacities(peer); @@ -701,21 +713,13 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority void setExternalListenHost(byte addr[]) throws UnknownHostException { _externalListenHost = InetAddress.getByAddress(addr); } - void addRelayPeer(String host, int port, byte tag[], SessionKey relayIntroKey) { - if ( (_externalListenPort > 0) && (_externalListenHost != null) ) - return; // no need for relay peers, as we are reachable - - RelayPeer peer = new RelayPeer(host, port, tag, relayIntroKey); - synchronized (_relayPeers) { - _relayPeers.add(peer); - } - } private boolean explicitAddressSpecified() { return (_context.getProperty(PROP_EXTERNAL_HOST) != null); } - void rebuildExternalAddress() { + void rebuildExternalAddress() { rebuildExternalAddress(true); } + void rebuildExternalAddress(boolean allowRebuildRouterInfo) { // if the external port is specified, we want to use that to bind to even // if we don't know the external host. String port = _context.getProperty(PROP_EXTERNAL_PORT); @@ -736,29 +740,49 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } } - Properties options = new Properties(); + Properties options = new Properties(); + boolean introducersRequired = introducersRequired(); + if (introducersRequired) { + List peers = new ArrayList(PUBLIC_RELAY_COUNT); + int found = 0; + _introManager.pickInbound(peers, PUBLIC_RELAY_COUNT); + if (_log.shouldLog(Log.INFO)) + _log.info("Introducers required, picked peers: " + peers); + for (int i = 0; i < peers.size(); i++) { + PeerState peer = (PeerState)peers.get(i); + RouterInfo ri = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer()); + if (ri == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Picked peer has no local routerInfo: " + peer); + continue; + } + RouterAddress ra = ri.getTargetAddress(STYLE); + if (ra == null) { + if (_log.shouldLog(Log.INFO)) + _log.info("Picked peer has no SSU address: " + ri); + continue; + } + UDPAddress ura = new UDPAddress(ra); + options.setProperty(UDPAddress.PROP_INTRO_HOST_PREFIX + i, peer.getRemoteHostId().toHostString()); + options.setProperty(UDPAddress.PROP_INTRO_PORT_PREFIX + i, String.valueOf(peer.getRemotePort())); + options.setProperty(UDPAddress.PROP_INTRO_KEY_PREFIX + i, Base64.encode(ura.getIntroKey())); + options.setProperty(UDPAddress.PROP_INTRO_TAG_PREFIX + i, String.valueOf(peer.getTheyRelayToUsAs())); + found++; + } + if (found > 0) { + if (_log.shouldLog(Log.INFO)) + _log.info("Picked peers: " + found); + _introducersSelectedOn = _context.clock().now(); + } + } if ( (_externalListenPort > 0) && (_externalListenHost != null) ) { options.setProperty(UDPAddress.PROP_PORT, String.valueOf(_externalListenPort)); options.setProperty(UDPAddress.PROP_HOST, _externalListenHost.getHostAddress()); // if we have explicit external addresses, they had better be reachable - options.setProperty(UDPAddress.PROP_CAPACITY, ""+UDPAddress.CAPACITY_TESTING); - } else { - // grab 3 relays randomly - synchronized (_relayPeers) { - Collections.shuffle(_relayPeers); - int numPeers = PUBLIC_RELAY_COUNT; - if (numPeers > _relayPeers.size()) - numPeers = _relayPeers.size(); - for (int i = 0; i < numPeers; i++) { - RelayPeer peer = (RelayPeer)_relayPeers.get(i); - options.setProperty("relay." + i + ".host", peer.getHost()); - options.setProperty("relay." + i + ".port", String.valueOf(peer.getPort())); - options.setProperty("relay." + i + ".tag", Base64.encode(peer.getTag())); - options.setProperty("relay." + i + ".key", peer.getIntroKey().toBase64()); - } - } - if (options.size() <= 0) - return; + if (introducersRequired) + options.setProperty(UDPAddress.PROP_CAPACITY, ""+UDPAddress.CAPACITY_TESTING); + else + options.setProperty(UDPAddress.PROP_CAPACITY, ""+UDPAddress.CAPACITY_TESTING + UDPAddress.CAPACITY_INTRODUCER); } options.setProperty(UDPAddress.PROP_INTRO_KEY, _introKey.toBase64()); @@ -768,10 +792,29 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority addr.setTransportStyle(STYLE); addr.setOptions(options); + boolean wantsRebuild = false; + if ( (_externalAddress == null) || !(_externalAddress.equals(addr)) ) + wantsRebuild = true; _externalAddress = addr; if (_log.shouldLog(Log.INFO)) _log.info("Address rebuilt: " + addr); replaceAddress(addr); + if (allowRebuildRouterInfo) + _context.router().rebuildRouterInfo(); + } + + public static final String PROP_FORCE_INTRODUCERS = "i2np.udp.forceIntroducers"; + public boolean introducersRequired() { + String forceIntroducers = _context.getProperty(PROP_FORCE_INTRODUCERS); + if ( (forceIntroducers != null) && (Boolean.valueOf(forceIntroducers).booleanValue()) ) + return true; + switch (getReachabilityStatus()) { + case CommSystemFacade.STATUS_REJECT_UNSOLICITED: + case CommSystemFacade.STATUS_DIFFERENT: + return true; + default: + return false; + } } String getPacketHandlerStatus() { @@ -869,7 +912,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority buf.append("
");
+ buf.append("");
buf.append("");
@@ -897,6 +940,15 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
buf.append("0");
buf.append(port);
buf.append("");
+ if (peer.getWeRelayToThemAs() > 0)
+ buf.append(">");
+ else
+ buf.append(" ");
+ if (peer.getTheyRelayToUsAs() > 0)
+ buf.append("<");
+ else
+ buf.append(" ");
+
boolean appended = false;
if (_activeThrottle.isChoked(peer.getRemotePeer())) {
if (!appended) buf.append("
");
@@ -1131,7 +1183,21 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
break;
}
}
- public short getReachabilityStatus() { return _reachabilityStatus; }
+ private static final String PROP_REACHABILITY_STATUS_OVERRIDE = "i2np.udp.status";
+ public short getReachabilityStatus() {
+ String override = _context.getProperty(PROP_REACHABILITY_STATUS_OVERRIDE);
+ if (override == null)
+ return _reachabilityStatus;
+
+ if ("ok".equals(override))
+ return CommSystemFacade.STATUS_OK;
+ else if ("err-reject".equals(override))
+ return CommSystemFacade.STATUS_REJECT_UNSOLICITED;
+ else if ("err-different".equals(override))
+ return CommSystemFacade.STATUS_DIFFERENT;
+
+ return _reachabilityStatus;
+ }
public void recheckReachability() {
_testEvent.runTest();
}