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 2a80676c7e2ba3c13cdc1c865ae41906610ab541..8ee70c1db1ffb0316ba79ec27b543c4c715cecb3 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -1,5 +1,6 @@ package net.i2p.router.transport.udp; +import java.net.DatagramPacket; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.GeneralSecurityException; @@ -10,7 +11,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.southernstorm.noise.protocol.ChaChaPolyCipherState; + import net.i2p.data.Base64; +import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.router.RouterAddress; import net.i2p.data.router.RouterIdentity; @@ -27,9 +31,11 @@ import net.i2p.router.transport.TransportUtil; import net.i2p.router.transport.crypto.DHSessionKeyBuilder; import static net.i2p.router.transport.udp.InboundEstablishState.InboundState.*; import static net.i2p.router.transport.udp.OutboundEstablishState.OutboundState.*; +import static net.i2p.router.transport.udp.SSU2Util.*; import net.i2p.router.util.DecayingHashSet; import net.i2p.router.util.DecayingBloomFilter; import net.i2p.util.Addresses; +import net.i2p.util.HexDump; import net.i2p.util.I2PThread; import net.i2p.util.LHMCache; import net.i2p.util.Log; @@ -1371,6 +1377,8 @@ class EstablishmentManager { * Called from UDPReceiver. * Accelerate response to RelayResponse if we haven't sent it yet. * + * SSU 1 only. + * * @since 0.9.15 */ void receiveHolePunch(InetAddress from, int fromPort) { @@ -1393,6 +1401,66 @@ class EstablishmentManager { } } + /** + * Called from PacketHandler. + * Accelerate response to RelayResponse if we haven't sent it yet. + * + * SSU 2 only. + * + * @param id non-null + * @param packet header already decrypted + * @since 0.9.55 + */ + void receiveHolePunch(RemoteHostId id, UDPPacket packet) { + DatagramPacket pkt = packet.getPacket(); + int off = pkt.getOffset(); + int len = pkt.getLength(); + byte data[] = pkt.getData(); + long rcvConnID = DataHelper.fromLong8(data, off); + long sendConnID = DataHelper.fromLong8(data, off + SRC_CONN_ID_OFFSET); + int type = data[off + TYPE_OFFSET] & 0xff; + if (type != HOLE_PUNCH_FLAG_BYTE) + return; + byte[] introKey = _transport.getSSU2StaticIntroKey(); + ChaChaPolyCipherState chacha = new ChaChaPolyCipherState(); + chacha.initializeKey(introKey, 0); + long n = DataHelper.fromLong(data, off + PKT_NUM_OFFSET, 4); + chacha.setNonce(n); + try { + // decrypt in-place + chacha.decryptWithAd(data, off, LONG_HEADER_SIZE, + data, off + LONG_HEADER_SIZE, data, off + LONG_HEADER_SIZE, len - LONG_HEADER_SIZE); + int payloadLen = len - (LONG_HEADER_SIZE + MAC_LEN); + SSU2Payload.PayloadCallback cb = new HPCallback(id); + SSU2Payload.processPayload(_context, cb, data, off + LONG_HEADER_SIZE, payloadLen, false); + // TODO process cb fields + } catch (Exception e) { + if (_log.shouldWarn()) + _log.warn("Bad HolePunch packet:\n" + HexDump.dump(data, off, len), e); + return; + } finally { + chacha.destroy(); + } + + // TODO now we can look up by nonce instead if we want + OutboundEstablishState state = _outboundStates.get(id); + if (state != null) { + boolean sendNow = state.receiveHolePunch(); + if (sendNow) { + if (_log.shouldDebug()) + _log.debug("Hole punch from " + state + ", sending SessionRequest now"); + notifyActivity(); + } else { + if (_log.shouldLog(Log.INFO)) + _log.info("Hole punch from " + state + ", already sent SessionRequest"); + } + } else { + // HolePunch received before RelayResponse, and we didn't know the IP/port, or it changed + if (_log.shouldLog(Log.INFO)) + _log.info("No state found for hole punch from " + id); + } + } + /** * Are IP and port valid? This is only for checking the relay response. * Allow IPv6 as of 0.9.50. @@ -1963,6 +2031,89 @@ class EstablishmentManager { } } + /** + * Process SSU2 hole punch payload + * + * @since 0.9.55 + */ + private class HPCallback implements SSU2Payload.PayloadCallback { + private final RemoteHostId _from; + public long _timeReceived; + public byte[] _aliceIP; + public int _alicePort; + public int _respCode; + public byte[] _respData; + + public HPCallback(RemoteHostId from) { + _from = from; + } + + public void gotDateTime(long time) { + _timeReceived = time; + } + + public void gotOptions(byte[] options, boolean isHandshake) {} + + public void gotRI(RouterInfo ri, boolean isHandshake, boolean flood) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotRIFragment(byte[] data, boolean isHandshake, boolean flood, boolean isGzipped, int frag, int totalFrags) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotAddress(byte[] ip, int port) { + _aliceIP = ip; + _alicePort = port; + } + + public void gotRelayTagRequest() { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotRelayTag(long tag) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotRelayRequest(byte[] data) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotRelayResponse(int status, byte[] data) { + _respCode = status; + _respData = data; + } + + public void gotRelayIntro(Hash aliceHash, byte[] data) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotPeerTest(int msg, int status, Hash h, byte[] data) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotToken(long token, long expires) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotI2NP(I2NPMessage msg) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotFragment(byte[] data, int off, int len, long messageId,int frag, boolean isLast) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotACK(long ackThru, int acks, byte[] ranges) { + throw new IllegalStateException("Bad block in HP"); + } + + public void gotTermination(int reason, long count) { + throw new IllegalStateException("Bad block in HP"); + } + } + + //// End SSU 2 //// 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 4d1f03c2837dda9ee373ca1ccba3fddc66a2be6e..5bf7f266dd051f43b0492de5f94e5485bbb38806 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java @@ -810,7 +810,7 @@ class PacketHandler { SSU2Header.Header header; int type; if (state == null) { - // Session Request, Token Request, or Peer Test + // Session Request, Token Request, Peer Test 5-7, or Hole Punch k2 = k1; header = SSU2Header.trialDecryptHandshakeHeader(packet, k1, k2); if (header == null || @@ -818,7 +818,7 @@ class PacketHandler { header.getVersion() != 2 || header.getNetID() != _networkID) { if (header != null && _log.shouldInfo()) - _log.info("Does not decrypt as Session Request, attempt to decrypt as Token Request/Peer Test: " + header + " from " + from); + _log.info("Does not decrypt as Session Request, attempt to decrypt as TokenRequest/PeerTest/HolePunch: " + header + " from " + from); // The first 32 bytes were fine, but it corrupted the next 32 bytes // TODO make this more efficient, just take the first 32 bytes header = SSU2Header.trialDecryptLongHeader(packet, k1, k2); @@ -916,6 +916,11 @@ class PacketHandler { _log.debug("Got a Peer Test"); if (SSU2Util.ENABLE_PEER_TEST) _testManager.receiveTest(from, packet); + } else if (type == SSU2Util.HOLE_PUNCH_FLAG_BYTE) { + if (_log.shouldDebug()) + _log.debug("Got a Hole Punch"); + if (SSU2Util.ENABLE_RELAY) + _establisher.receiveHolePunch(from, packet); } else { if (_log.shouldWarn()) _log.warn("Got unknown message " + header + " on " + state);