diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java index 88e9c258c4236e4afe0c725b05661449b4543b4d..1013de01b15bd487fab55e169c85efad0fb4c94a 100644 --- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java +++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java @@ -13,7 +13,11 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; import net.i2p.data.SessionKey; +import net.i2p.data.SigningPrivateKey; +import net.i2p.data.SigningPublicKey; import net.i2p.data.router.RouterAddress; import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; @@ -437,13 +441,15 @@ class IntroductionManager { /** * We are Charlie and we got this from Bob. - * Send a HolePunch to Alice, who will soon be sending us a RelayRequest. + * Send a HolePunch to Alice, who will soon be sending us a SessionRequest. * We should already have a session with Bob, but probably not with Alice. * * If we don't have a session with Bob, we removed the relay tag from * our _outbound table, so this won't work. * * We do some throttling here. + * + * SSU 1 only. */ void receiveRelayIntro(RemoteHostId bob, UDPPacketReader reader) { if (_context.router().isHidden()) @@ -541,6 +547,8 @@ class IntroductionManager { * We are Bob and we got this from Alice. * Send a RelayIntro to Charlie and a RelayResponse to Alice. * We should already have a session with Charlie, but not necessarily with Alice. + * + * SSU 1 only. */ void receiveRelayRequest(RemoteHostId alice, UDPPacketReader reader) { if (_context.router().isHidden()) @@ -649,6 +657,155 @@ class IntroductionManager { cipherKey, macKey)); } + /** + * We are Bob and we got this from Alice. + * Send Alice's RI and a RelayIntro to Charlie, or reject with a RelayResponse to Alice. + * We should already have a session with Charlie and definitely with Alice. + * + * SSU 2 only. + * + * @since 0.9.55 + */ + void receiveRelayRequest(PeerState2 alice, byte[] data) { + } + + /** + * We are Charlie and we got this from Bob. + * Send a HolePunch to Alice, who will soon be sending us a SessionRequest. + * And send a RelayResponse to bob. + * + * SSU 2 only. + * + * @since 0.9.55 + */ + void receiveRelayIntro(PeerState2 bob, Hash alice, byte[] data) { + long nonce = DataHelper.fromLong(data, 0, 4); + long tag = DataHelper.fromLong(data, 4, 4); + long time = DataHelper.fromLong(data, 8, 4) * 1000; + int ver = data[12] & 0xff; + if (ver != 2) { + if (_log.shouldWarn()) + _log.warn("Bad relay intro version " + ver + " from " + bob); + return; + } + int iplen = data[13] & 0xff; + if (iplen != 6 && iplen != 18) { + if (_log.shouldWarn()) + _log.warn("Bad IP length " + iplen + " from " + bob); + return; + } + boolean isIPv6 = iplen == 18; + int testPort = (int) DataHelper.fromLong(data, 14, 2); + byte[] testIP = new byte[iplen - 2]; + System.arraycopy(data, 16, testIP, 0, iplen - 2); + InetAddress aliceIP; + try { + aliceIP = InetAddress.getByAddress(testIP); + } catch (UnknownHostException uhe) { + return; + } + + RouterInfo aliceRI = null; + SessionKey aliceIntroKey = null; + int rcode; + PeerState aps = _transport.getPeerState(alice); + if (aps != null && aps.isIPv6() == isIPv6) { + rcode = SSU2Util.RELAY_REJECT_CHARLIE_CONNECTED; + } else if (_context.banlist().isBanlisted(alice)) { + rcode = SSU2Util.RELAY_REJECT_CHARLIE_BANNED; + } else if (!TransportUtil.isValidPort(testPort) || + !_transport.isValid(testIP) || + _transport.isTooClose(testIP) || + _context.blocklist().isBlocklisted(testIP)) { + rcode = SSU2Util.RELAY_REJECT_CHARLIE_ADDRESS; + } else { + // bob should have sent it to us. Don't bother to lookup + // remotely if he didn't, or it was out-of-order or lost. + aliceRI = _context.netDb().lookupRouterInfoLocally(alice); + if (aliceRI != null) { + // validate signed data + SigningPublicKey spk = aliceRI.getIdentity().getSigningPublicKey(); + if (SSU2Util.validateSig(_context, SSU2Util.RELAY_REQUEST_PROLOGUE, + bob.getRemotePeer(), _context.routerHash(), data, spk)) { + aliceIntroKey = PeerTestManager.getIntroKey(getAddress(aliceRI, isIPv6)); + if (aliceIntroKey != null) + rcode = SSU2Util.RELAY_ACCEPT; + else + rcode = SSU2Util.RELAY_REJECT_CHARLIE_ADDRESS; + } else { + if (_log.shouldWarn()) + _log.warn("Signature failed relay intro\n" + aliceRI); + rcode = SSU2Util.RELAY_REJECT_CHARLIE_SIGFAIL; + } + } else { + if (_log.shouldWarn()) + _log.warn("Alice RI not found " + alice); + rcode = SSU2Util.RELAY_REJECT_CHARLIE_UNKNOWN_ALICE; + } + } + + // generate our signed data + // we sign it even if rejecting, not required though + SigningPrivateKey spk = _context.keyManager().getSigningPrivateKey(); + data = SSU2Util.createRelayResponseData(_context, bob.getRemotePeer(), rcode, + nonce, testIP, testPort, spk); + if (data == null) { + if (_log.shouldWarn()) + _log.warn("sig fail"); + return; + } + UDPPacket packet = _builder2.buildRelayResponse(data, bob); + if (_log.shouldDebug()) + _log.debug("Send relay response " + " nonce " + nonce + " to " + bob); + _transport.send(packet); + if (rcode == SSU2Util.RELAY_ACCEPT) { + // send hole punch with the same data we sent to Bob + if (_log.shouldDebug()) + _log.debug("Send hole punch to " + Addresses.toString(testIP, testPort)); + long rcvId = (nonce << 32) | nonce; + long sendId = ~rcvId; + packet = _builder2.buildHolePunch(aliceIP, testPort, aliceIntroKey, sendId, rcvId, data); + _transport.send(packet); + } + } + + /** + * We are Bob and we got this from Charlie, OR + * we are Alice and we got this from Bob. + * + * If we are Bob, send to Alice. + * If we are Alice, send a SessionRequest to Charlie. + * We should already have a session with Charlie, but not necessarily with Alice. + * + * SSU 2 only. + * + * @since 0.9.55 + */ + void receiveRelayResponse(PeerState2 peer, int status, byte[] data) { + } + + /** + * We are Alice and we got this from Charlie. + * Send a SessionRequest to Charlie, whether or not we got the Relay Response already. + * + * SSU 2 only, out-of-session. + * + * @since 0.9.55 + */ + void receiveHolePunch(RemoteHostId charlie, byte[] data) { + } + + /** + * Get an address out of a RI. SSU2 only. + * + * @return address or null + * @since 0.9.55 + */ + private RouterAddress getAddress(RouterInfo ri, boolean isIPv6) { + List<RouterAddress> addrs = _transport.getTargetAddresses(ri); + return PeerTestManager.getAddress(addrs, isIPv6); + } + /** * Are IP and port valid? * Reject all IPv6, for now, even if we are configured for it. diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java index b1da0f0ad9255cbeb677f5235fb5c0de27230e6d..d70f8ff9a6cd4ff56e70f34668fe492df486cbba 100644 --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder2.java @@ -761,19 +761,22 @@ class PacketBuilder2 { } /** - * Creates an empty unauthenticated packet for hole punching. - * Parameters must be validated previously. + * Out-of-session, containing a RelayResponse block. + * */ - public UDPPacket buildHolePunch(InetAddress to, int port) { - UDPPacket packet = UDPPacket.acquire(_context, false); + public UDPPacket buildHolePunch(InetAddress to, int port, SessionKey introKey, + long sendID, long rcvID, byte[] signedData) { + long n = _context.random().signedNextInt() & 0xFFFFFFFFL; + long token = _context.random().nextLong(); + UDPPacket packet = buildLongPacketHeader(sendID, n, HOLE_PUNCH_FLAG_BYTE, rcvID, token); + Block block = new SSU2Payload.RelayResponseBlock(signedData); 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); + byte[] ik = introKey.getData(); + packet.getPacket().setLength(LONG_HEADER_SIZE); + encryptPeerTest(packet, ik, n, ik, ik, to.getAddress(), port, block); setTo(packet, to, port); - packet.setMessageType(TYPE_PUNCH); packet.setPriority(PRIORITY_HIGH); return packet; diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState2.java b/router/java/src/net/i2p/router/transport/udp/PeerState2.java index d4eea32d1fac9f09e61af0a2cc74650fc2ee22fb..1b35bb2824e5e90bc3ece7cfc19d79f25afc36c8 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerState2.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerState2.java @@ -434,6 +434,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback public void gotRelayRequest(byte[] data) { if (!ENABLE_RELAY) return; + _transport.getIntroManager().receiveRelayRequest(this, data); // Relay blocks are ACK-eliciting messagePartiallyReceived(); } @@ -441,6 +442,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback public void gotRelayResponse(int status, byte[] data) { if (!ENABLE_RELAY) return; + _transport.getIntroManager().receiveRelayResponse(this, status, data); // Relay blocks are ACK-eliciting messagePartiallyReceived(); } @@ -448,6 +450,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback public void gotRelayIntro(Hash aliceHash, byte[] data) { if (!ENABLE_RELAY) return; + _transport.getIntroManager().receiveRelayIntro(this, aliceHash, data); // Relay blocks are ACK-eliciting messagePartiallyReceived(); } 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 a26f7c5aae665b36f456f3e06e6bf6be26ca6919..86fb060bd4542e29bdb5646b622a3ddc32c1a1ba 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java @@ -1336,10 +1336,21 @@ class PeerTestManager { /** * Get an address out of a RI. SSU2 only. * + * @return address or null * @since 0.9.54 */ private RouterAddress getAddress(RouterInfo ri, boolean isIPv6) { List<RouterAddress> addrs = _transport.getTargetAddresses(ri); + return getAddress(addrs, isIPv6); + } + + /** + * Get an address out of a list of addresses. SSU2 only. + * + * @return address or null + * @since 0.9.55 + */ + static RouterAddress getAddress(List<RouterAddress> addrs, boolean isIPv6) { RouterAddress ra = null; for (RouterAddress addr : addrs) { // skip SSU 1 address w/o "s" @@ -1367,9 +1378,9 @@ class PeerTestManager { /** * Get an intro key out of an address. SSU2 only. * - * @since 0.9.54 + * @since 0.9.54, pkg private since 0.9.55 for IntroManager */ - private static SessionKey getIntroKey(RouterAddress ra) { + static SessionKey getIntroKey(RouterAddress ra) { if (ra == null) return null; String siv = ra.getOption("i"); diff --git a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java index f2b357e372252c34d881c87754994dfb0d1d591f..15a6be50d7642d1ae9e5de9609e42ae7e1738652 100644 --- a/router/java/src/net/i2p/router/transport/udp/SSU2Util.java +++ b/router/java/src/net/i2p/router/transport/udp/SSU2Util.java @@ -106,6 +106,7 @@ final class SSU2Util { public static final byte PEER_TEST_FLAG_BYTE = UDPPacket.PAYLOAD_TYPE_TEST; public static final byte RETRY_FLAG_BYTE = 9; public static final byte TOKEN_REQUEST_FLAG_BYTE = 10; + public static final byte HOLE_PUNCH_FLAG_BYTE = 11; public static final String INFO_CREATED = "SessCreateHeader"; public static final String INFO_CONFIRMED = "SessionConfirmed"; @@ -132,6 +133,20 @@ final class SSU2Util { public static final int TEST_REJECT_CHARLIE_BANNED = 69; public static final int TEST_REJECT_CHARLIE_UNKNOWN_ALICE = 70; + public static final int RELAY_ACCEPT = 0; + public static final int RELAY_REJECT_BOB_UNSPEC = 1; + public static final int RELAY_REJECT_BOB_BANNED_CHARLIE = 2; + public static final int RELAY_REJECT_BOB_LIMIT = 3; + public static final int RELAY_REJECT_BOB_SIGFAIL = 4; + public static final int RELAY_REJECT_BOB_NO_TAG = 5; + public static final int RELAY_REJECT_CHARLIE_UNSPEC = 64; + public static final int RELAY_REJECT_CHARLIE_ADDRESS = 65; + public static final int RELAY_REJECT_CHARLIE_LIMIT = 66; + public static final int RELAY_REJECT_CHARLIE_SIGFAIL = 67; + public static final int RELAY_REJECT_CHARLIE_CONNECTED = 68; + public static final int RELAY_REJECT_CHARLIE_BANNED = 69; + public static final int RELAY_REJECT_CHARLIE_UNKNOWN_ALICE = 70; + // termination reason codes public static final int REASON_UNSPEC = 0; public static final int REASON_TERMINATION = 1; @@ -199,6 +214,65 @@ final class SSU2Util { return data; } + /** + * Make the data for the relay request block + * + * @param h Bob hash to be included in sig, not included in data + * @param h2 Charlie hash to be included in sig, not included in data + * @param ip non-null + * @return null on failure + * @since 0.9.55 + */ + public static byte[] createRelayRequestData(I2PAppContext ctx, Hash h, Hash h2, + long nonce, long tag, byte[] ip, int port, + SigningPrivateKey spk) { + int datalen = 17 + ip.length; + byte[] data = new byte[datalen + spk.getType().getSigLen()]; + //data[0] = 0; // flag + DataHelper.toLong(data, 1, 4, nonce); + DataHelper.toLong(data, 5, 4, tag); + DataHelper.toLong(data, 9, 4, ctx.clock().now() / 1000); + data[13] = 2; // version + data[14] = (byte) (ip.length + 2); + DataHelper.toLong(data, 15, 2, port); + System.arraycopy(ip, 0, data, 17, ip.length); + Signature sig = sign(ctx, RELAY_REQUEST_PROLOGUE, h, h2, data, datalen, spk); + if (sig == null) + return null; + byte[] s = sig.getData(); + System.arraycopy(s, 0, data, datalen, s.length); + return data; + } + + /** + * Make the data for the relay response block + * + * @param h Bob hash to be included in sig, not included in data + * @param ip non-null + * @return null on failure + * @since 0.9.55 + */ + public static byte[] createRelayResponseData(I2PAppContext ctx, Hash h, int code, + long nonce, byte[] ip, int port, + SigningPrivateKey spk) { + int datalen = 14 + ip.length; + byte[] data = new byte[datalen + spk.getType().getSigLen()]; + //data[0] = 0; // flag + data[1] = (byte) code; + DataHelper.toLong(data, 2, 4, nonce); + DataHelper.toLong(data, 6, 4, ctx.clock().now() / 1000); + data[10] = 2; // version + data[11] = (byte) (ip.length + 2); + DataHelper.toLong(data, 12, 2, port); + System.arraycopy(ip, 0, data, 14, ip.length); + Signature sig = sign(ctx, RELAY_RESPONSE_PROLOGUE, h, null, data, datalen, spk); + if (sig == null) + return null; + byte[] s = sig.getData(); + System.arraycopy(s, 0, data, datalen, s.length); + return data; + } + /** * Sign the relay or peer test data, using * the prologue and hash as the initial data,