SSU2: Relay WIP part 1

Pass relay data from PS2 to Intro manager
Handle relay intro as charlie
Send hole punch as charlie
Send relay response as charlie
Update hole punch format to match spec
This commit is contained in:
zzz
2022-06-01 13:12:35 -04:00
parent a4d3bf285d
commit 8b0edde290
5 changed files with 259 additions and 11 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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");

View File

@@ -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,