Merge branch 'ssu1-4' into 'master'

Transport: SSU1 removal part 4/n

See merge request i2p-hackers/i2p.i2p!186
This commit is contained in:
zzz
2024-04-22 19:41:58 +00:00
@@ -85,7 +85,6 @@ class IntroductionManager {
private final RouterContext _context;
private final Log _log;
private final UDPTransport _transport;
private final PacketBuilder _builder;
private final PacketBuilder2 _builder2;
/** map of relay tag to Charlie PeerState that should receive the introduction (we are Bob) */
private final Map<Long, PeerState> _outbound;
@@ -115,15 +114,11 @@ class IntroductionManager {
private static final long INTRODUCER_EXPIRATION = 80*60*1000L;
private static final String MIN_IPV6_INTRODUCER_VERSION = "0.9.50";
private static final long MAX_SKEW = 2*60*1000;
/** testing */
private static final String PROP_PREFER_SSU2 = "i2np.ssu2.preferSSU2Introducers";
private static final boolean DEFAULT_PREFER_SSU2 = true;
public IntroductionManager(RouterContext ctx, UDPTransport transport) {
_context = ctx;
_log = ctx.logManager().getLog(IntroductionManager.class);
_transport = transport;
_builder = transport.getBuilder();
_builder2 = transport.getBuilder2();
_outbound = new ConcurrentHashMap<Long, PeerState>(MAX_OUTBOUND);
_inbound = new ConcurrentHashMap<Long, PeerState>(MAX_INBOUND);
@@ -150,9 +145,7 @@ class IntroductionManager {
if (added)
_outbound.put(Long.valueOf(id), peer);
long id2 = peer.getTheyRelayToUsAs();
//if (id2 > 0 && _inbound.size() < MAX_INBOUND) {
// test
if (id2 > 0 && (_inbound.size() < MAX_INBOUND || peer.getVersion() == 2)) {
if (id2 > 0 && _inbound.size() < MAX_INBOUND) {
added = true;
_inbound.put(Long.valueOf(id2), peer);
}
@@ -210,8 +203,7 @@ class IntroductionManager {
if (_inbound.isEmpty()) return 0;
List<PeerState> peers = new ArrayList<PeerState>(_inbound.values());
int sz = peers.size();
boolean preferV2 = _builder2 != null && _context.getProperty(PROP_PREFER_SSU2, DEFAULT_PREFER_SSU2);
Collections.sort(peers, new PeerStateComparator(preferV2));
Collections.sort(peers, new PeerStateComparator());
int found = 0;
long now = _context.clock().now();
long inactivityCutoff = now - (UDPTransport.EXPIRE_TIMEOUT / 2); // 15 min
@@ -221,8 +213,6 @@ class IntroductionManager {
List<Introducer> introducers = new ArrayList<Introducer>(howMany);
String exp = Long.toString((now + INTRODUCER_EXPIRATION) / 1000);
// try to keep a mix of v1 and v2
int ssu1count = 0;
int ssu2count = 0;
// reuse old ones if ok
if (current != null) {
@@ -235,29 +225,10 @@ class IntroductionManager {
if (!isInboundTagValid(tag))
continue;
String sexp = Long.toString(ua.getIntroducerExpiration(i) / 1000);
Introducer intro;
byte[] key = ua.getIntroducerKey(i);
if (key != null) {
// SSU 1
//// Replace SSU 1 with SSU 2 if available for slot 2
//// leave slots 0 and 1 for SSU 1
//// this will churn the SSU 1 introducers, oh well
if (preferV2 && ssu1count >= 2)
continue;
intro = new Introducer(ua.getIntroducerHost(i).getAddress(),
ua.getIntroducerPort(i), key, tag, sexp);
ssu1count++;
if (_log.shouldInfo())
_log.info("Reusing introducer: " + ua.getIntroducerHost(i));
} else {
// SSU 2
if (_builder != null && ssu2count >= 2)
continue;
intro = new Introducer(ua.getIntroducerHash(i), tag, sexp);
ssu2count++;
if (_log.shouldInfo())
_log.info("Reusing introducer: " + ua.getIntroducerHash(i));
}
Introducer intro = new Introducer(ua.getIntroducerHash(i), tag, sexp);
ssu2count++;
if (_log.shouldInfo())
_log.info("Reusing introducer: " + ua.getIntroducerHash(i));
introducers.add(intro);
found++;
}
@@ -270,14 +241,10 @@ class IntroductionManager {
continue;
Hash hash = cur.getRemotePeer();
// dup check of reused SSU2 introducers
if (cur.getVersion() > 1) {
String b64 = hash.toBase64();
for (Introducer intro : introducers) {
if (b64.equals(intro.shash))
continue outerloop;
}
if (_builder != null && ssu2count >= 2)
continue;
String b64 = hash.toBase64();
for (Introducer intro : introducers) {
if (b64.equals(intro.shash))
continue outerloop;
}
RouterInfo ri = _context.netDb().lookupRouterInfoLocally(hash);
if (ri == null) {
@@ -318,7 +285,6 @@ class IntroductionManager {
continue;
}
int oldFound = found;
loop:
for (RouterAddress ra : ras) {
byte[] ip = ra.getIP();
if (ip == null)
@@ -327,11 +293,6 @@ class IntroductionManager {
String host = ip.length == 4 ? ra.getHost() : Addresses.toString(ip);
if (host == null)
continue;
// dup check of reused introducers
for (Introducer intro : introducers) {
if (host.equals(intro.sip))
continue loop;
}
int port = ra.getPort();
if (!isValid(ip, port, true))
continue;
@@ -343,23 +304,11 @@ class IntroductionManager {
continue;
}
cur.setIntroducerTime();
Introducer intro;
if (cur.getVersion() == 1) {
UDPAddress ura = new UDPAddress(ra);
byte[] ikey = ura.getIntroKey();
if (ikey == null)
continue;
intro = new Introducer(ip, port, ikey, cur.getTheyRelayToUsAs(), exp);
ssu1count++;
} else {
intro = new Introducer(hash, cur.getTheyRelayToUsAs(), exp);
ssu2count++;
}
Introducer intro = new Introducer(hash, cur.getTheyRelayToUsAs(), exp);
ssu2count++;
introducers.add(intro);
found++;
// two per router max, one for SSU 2
if (found - oldFound >= 2 || cur.getVersion() > 1)
break;
break;
}
if (oldFound != found && _log.shouldLog(Log.INFO))
_log.info("Picking introducer: " + cur);
@@ -369,13 +318,7 @@ class IntroductionManager {
Collections.sort(introducers);
for (int i = 0; i < found; i++) {
Introducer in = introducers.get(i);
if (in.version == 1) {
ssuOptions.setProperty(UDPAddress.PROP_INTRO_HOST_PREFIX + i, in.sip);
ssuOptions.setProperty(UDPAddress.PROP_INTRO_PORT_PREFIX + i, in.sport);
ssuOptions.setProperty(UDPAddress.PROP_INTRO_KEY_PREFIX + i, in.skey);
} else {
ssuOptions.setProperty(UDPAddress.PROP_INTRO_HASH_PREFIX + i, in.shash);
}
ssuOptions.setProperty(UDPAddress.PROP_INTRO_HASH_PREFIX + i, in.shash);
ssuOptions.setProperty(UDPAddress.PROP_INTRO_TAG_PREFIX + i, in.stag);
String sexp = in.sexp;
// look for existing expiration in current published
@@ -383,20 +326,10 @@ class IntroductionManager {
if (current != null) {
for (int j = 0; j < UDPTransport.PUBLIC_RELAY_COUNT; j++) {
String oexp = null;
if (in.version == 1) {
if (in.sip.equals(current.getOption(UDPAddress.PROP_INTRO_HOST_PREFIX + j)) &&
in.sport.equals(current.getOption(UDPAddress.PROP_INTRO_PORT_PREFIX + j)) &&
in.skey.equals(current.getOption(UDPAddress.PROP_INTRO_KEY_PREFIX + j)) &&
in.stag.equals(current.getOption(UDPAddress.PROP_INTRO_TAG_PREFIX + j))) {
// found old one
oexp = current.getOption(UDPAddress.PROP_INTRO_EXP_PREFIX + j);
}
} else {
if (in.shash.equals(current.getOption(UDPAddress.PROP_INTRO_HASH_PREFIX + j)) &&
in.stag.equals(current.getOption(UDPAddress.PROP_INTRO_TAG_PREFIX + j))) {
// found old one
oexp = current.getOption(UDPAddress.PROP_INTRO_EXP_PREFIX + j);
}
if (in.shash.equals(current.getOption(UDPAddress.PROP_INTRO_HASH_PREFIX + j)) &&
in.stag.equals(current.getOption(UDPAddress.PROP_INTRO_TAG_PREFIX + j))) {
// found old one
oexp = current.getOption(UDPAddress.PROP_INTRO_EXP_PREFIX + j);
}
if (oexp != null) {
try {
@@ -421,25 +354,14 @@ class IntroductionManager {
/**
* For picking introducers.
* Reverse sort, version 2 first, for testing
* Then lowest uptime first, to reduce idle timeout and disconnect,
* Lowest uptime first, to reduce idle timeout and disconnect,
* and ensure variety.
*
* @since 0.9.55
*/
private static class PeerStateComparator implements Comparator<PeerState> {
private final boolean _v2;
public PeerStateComparator(boolean preferV2) {
_v2 = preferV2;
}
public int compare(PeerState l, PeerState r) {
if (_v2) {
int rv = r.getVersion() - l.getVersion();
if (rv != 0)
return rv;
}
long d = r.getKeyEstablishedTime() - l.getKeyEstablishedTime();
if (d < 0)
return -1;
@@ -454,21 +376,7 @@ class IntroductionManager {
* @since 0.9.18
*/
private static class Introducer implements Comparable<Introducer> {
public final String sip, sport, skey, stag, sexp, shash;
public final int version;
/**
* SSU 1
*/
public Introducer(byte[] ip, int port, byte[] key, long tag, String exp) {
sip = Addresses.toString(ip);
sport = String.valueOf(port);
skey = Base64.encode(key);
stag = String.valueOf(tag);
sexp = exp;
version = 1;
shash = null;
}
public final String stag, sexp, shash;
/**
* SSU 2
@@ -478,18 +386,10 @@ class IntroductionManager {
stag = String.valueOf(tag);
sexp = exp;
shash = h.toBase64();
version = 2;
sip = null;
sport = null;
skey = null;
}
@Override
public int compareTo(Introducer i) {
// put SSU 2 at the end to not confuse SSU 1
int diff = version - i.version;
if (diff != 0)
return diff;
return stag.compareTo(i.stag);
}
@@ -530,10 +430,7 @@ class IntroductionManager {
cur.setLastPingTime(now);
UDPPacket ping;
try {
if (cur.getVersion() == 2)
ping = _builder2.buildPing((PeerState2) cur);
else
ping = _builder.buildPing(cur);
ping = _builder2.buildPing((PeerState2) cur);
_transport.send(ping);
} catch (IOException ioe) {
iter.remove();
@@ -569,233 +466,6 @@ class IntroductionManager {
return _outbound.size();
}
/**
* We are Charlie and we got this from Bob.
* 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())
return;
_context.statManager().addRateData("udp.receiveRelayIntro", 1);
if (!_transport.allowConnection()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping RelayIntro, over conn limit");
return;
}
int ipSize = reader.getRelayIntroReader().readIPSize();
byte ip[] = new byte[ipSize];
reader.getRelayIntroReader().readIP(ip, 0);
int port = reader.getRelayIntroReader().readPort();
// allow IPv6 as of 0.9.50
// validate alice IP/port here. We don't need to validate Bob, we have a session with him.
if (!isValid(ip, port, true)) {
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid relay intro for alice " + Addresses.toString(ip, port) + " via bob " + bob);
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
if (_log.shouldDebug())
_log.debug("Receive relay intro from " + bob + " for " + Addresses.toString(ip, port));
InetAddress to = null;
try {
to = InetAddress.getByAddress(ip);
} catch (UnknownHostException uhe) {
// banlist Bob?
if (_log.shouldLog(Log.WARN))
_log.warn("IP for alice to hole punch to is invalid", uhe);
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
RemoteHostId alice = new RemoteHostId(ip, port);
if (_transport.getPeerState(alice) != null) {
if (_log.shouldLog(Log.INFO))
_log.info("Ignoring RelayIntro, already have a session to " + to);
return;
}
EstablishmentManager establisher = _transport.getEstablisher();
if (establisher != null) {
if (establisher.getInboundState(alice) != null) {
// This check may be common, as Alice sends RelayRequests to
// several introducers at once.
if (_log.shouldLog(Log.INFO))
_log.info("Ignoring RelayIntro, establishment in progress to " + to);
return;
}
if (!establisher.shouldAllowInboundEstablishment()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping RelayIntro, too many establishments in progress - for " + to);
return;
}
}
// basic throttle, don't bother saving per-peer send times
// we throttle on IP only, ignoring port
boolean tooMany = false;
boolean already = false;
synchronized (_recentHolePunches) {
long now = _context.clock().now();
if (now > _lastHolePunchClean + PUNCH_CLEAN_TIME) {
_recentHolePunches.clear();
_lastHolePunchClean = now;
_recentHolePunches.add(to);
} else {
tooMany = _recentHolePunches.size() >= MAX_PUNCHES;
if (!tooMany)
already = !_recentHolePunches.add(to);
}
}
if (tooMany) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping - too many - RelayIntro for " + to);
return;
}
if (already) {
// This check will trigger a lot, as Alice sends RelayRequests to
// several introducers at once.
if (_log.shouldLog(Log.INFO))
_log.info("Ignoring dup RelayIntro for " + to);
return;
}
_transport.send(_builder.buildHolePunch(to, port));
}
/**
* 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())
return;
UDPPacketReader.RelayRequestReader rrReader = reader.getRelayRequestReader();
long tag = rrReader.readTag();
int ipSize = rrReader.readIPSize();
int port = rrReader.readPort();
byte[] aliceIP = alice.getIP();
int alicePort = alice.getPort();
boolean ipIncluded = ipSize != 0;
// here we allow IPv6
if (!isValid(aliceIP, alicePort, true)) {
// not necessarily invalid ip/port, could be blocklisted
if (_log.shouldWarn())
_log.warn("Rejecting relay req from " + alice + " for " + Addresses.toString(aliceIP, alicePort));
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
// prior to 0.9.24 we rejected any non-zero-length ip
// here we reject anything different if it's the same size
// As of 0.9.50 we allow relay request over IPv6
if (ipIncluded) {
byte ip[] = new byte[ipSize];
rrReader.readIP(ip, 0);
if (ipSize == aliceIP.length && !Arrays.equals(aliceIP, ip)) {
if (_log.shouldWarn())
_log.warn("Bad relay req from " + alice + " for " + Addresses.toString(ip, port));
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
aliceIP = ip;
}
// prior to 0.9.24 we rejected any nonzero port
// here we reject anything different
// As of 0.9.50 we allow it if the IP was included
if (port != 0) {
if (ipIncluded) {
alicePort = port;
} else if (port != alicePort) {
if (_log.shouldWarn())
_log.warn("Bad relay req from " + alice + " for " + Addresses.toString(aliceIP, port));
_context.statManager().addRateData("udp.relayBadIP", 1);
}
return;
}
// check again if IP was provided
// allow IPv6 as of 0.9.50
RemoteHostId aliceRelayID;
if (ipIncluded) {
if (!isValid(aliceIP, alicePort, true)) {
if (_log.shouldWarn())
_log.warn("Bad relay req from " + alice + " for " + Addresses.toString(aliceIP, alicePort));
_context.statManager().addRateData("udp.relayBadIP", 1);
return;
}
aliceRelayID = new RemoteHostId(aliceIP, alicePort);
} else {
aliceRelayID = alice;
}
PeerState charlie = get(tag);
if (charlie == null) {
if (_log.shouldDebug())
_log.debug("Receive relay request from " + alice
+ " with unknown tag " + tag);
_context.statManager().addRateData("udp.receiveRelayRequestBadTag", 1);
return;
}
if (charlie.getVersion() != 1) {
if (_log.shouldWarn())
_log.warn("Receive SSU1 relay request from " + alice + " for SSU2 " + charlie);
return;
}
if (_log.shouldDebug())
_log.debug("Receive relay request from " + alice
+ " for tag " + tag
+ " and relaying with " + charlie);
// TODO throttle based on alice identity and/or intro tag?
_context.statManager().addRateData("udp.receiveRelayRequest", 1);
// send that peer an introduction for alice
_transport.send(_builder.buildRelayIntro(aliceRelayID, charlie, rrReader));
long now = _context.clock().now();
charlie.setLastSendTime(now);
// send alice back charlie's info
// lookup session so we can use session key if available
SessionKey cipherKey = null;
SessionKey macKey = null;
PeerState aliceState = _transport.getPeerState(alice);
if (aliceState != null) {
// established session (since 0.9.12)
cipherKey = aliceState.getCurrentCipherKey();
macKey = aliceState.getCurrentMACKey();
}
if (cipherKey == null || macKey == null) {
// no session, use intro key (was only way before 0.9.12)
byte key[] = new byte[SessionKey.KEYSIZE_BYTES];
reader.getRelayRequestReader().readAliceIntroKey(key, 0);
cipherKey = new SessionKey(key);
macKey = cipherKey;
if (_log.shouldDebug())
_log.debug("Sending relay response (w/ intro key) to " + alice);
} else {
if (_log.shouldDebug())
_log.debug("Sending relay response (in-session) to " + alice);
aliceState.setLastSendTime(now);
}
_transport.send(_builder.buildRelayResponse(alice, charlie, rrReader.readNonce(),
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.