SSU: Enable RelayRequest over IPv6

This supports IPv6 introducers.
This requires that Alice (the requester) include the IPv4 address in the RelayRequest
when sent over IPv6, and that Bob (the introducer) uses that address instead of
the source address.
IPv6 address will be published in ihost[0-2].
This was specified as of 0.9.24 but never implemented by Java or i2pd.
Bob-Charlie and Alice-Charlie comms must still be over IPv4.
WIP, not fully tested.
IPv6 introductions is part 2, TBD.
ref: http://zzz.i2p/topics/3060
This commit is contained in:
zzz
2021-02-25 06:34:57 -05:00
parent c609e43d90
commit 9c677eb465
4 changed files with 126 additions and 72 deletions

View File

@@ -690,9 +690,9 @@ public abstract class TransportImpl implements Transport {
* shuffled and then sorted by cost/preference.
* Lowest cost (most preferred) first.
* @return non-null, possibly empty
* @since IPv6
* @since IPv6, public since 0.9.50, was protected
*/
protected List<RouterAddress> getTargetAddresses(RouterInfo target) {
public List<RouterAddress> getTargetAddresses(RouterInfo target) {
List<RouterAddress> rv;
String alt = getAltStyle();
if (alt != null)

View File

@@ -13,14 +13,15 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.Base64;
import net.i2p.data.SessionKey;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.SessionKey;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.TransportUtil;
import net.i2p.util.Addresses;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.router.transport.TransportUtil;
import net.i2p.util.VersionComparator;
/**
* Keep track of inbound and outbound introductions.
@@ -100,6 +101,7 @@ class IntroductionManager {
/** Max for all targets per PUNCH_CLEAN_TIME */
private static final int MAX_PUNCHES = 8;
private static final long INTRODUCER_EXPIRATION = 80*60*1000L;
private static final String MIN_IPV6_INTRODUCER_VERSION = "0.9.50";
public IntroductionManager(RouterContext ctx, UDPTransport transport) {
_context = ctx;
@@ -125,30 +127,32 @@ class IntroductionManager {
// let's not use an introducer on a privileged port, sounds like trouble
if (!TransportUtil.isValidPort(peer.getRemotePort()))
return;
// Only allow relay as Bob or Charlie if the Bob-Charlie session is IPv4
if (peer.getRemoteIP().length != 4)
return;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Adding peer " + peer.getRemoteHostId() + ", weRelayToThemAs "
+ peer.getWeRelayToThemAs() + ", theyRelayToUsAs " + peer.getTheyRelayToUsAs());
if (peer.getWeRelayToThemAs() > 0)
_outbound.put(Long.valueOf(peer.getWeRelayToThemAs()), peer);
if (peer.getTheyRelayToUsAs() > 0 && _inbound.size() < MAX_INBOUND) {
long id = peer.getWeRelayToThemAs();
boolean added = id > 0;
if (added)
_outbound.put(Long.valueOf(id), peer);
long id2 = peer.getTheyRelayToUsAs();
if (id2 > 0 && _inbound.size() < MAX_INBOUND) {
added = true;
_inbound.add(peer);
}
if (added &&_log.shouldLog(Log.DEBUG))
_log.debug("adding peer " + peer.getRemoteHostId() + ", weRelayToThemAs "
+ id + ", theyRelayToUsAs " + id2);
}
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());
long id = peer.getWeRelayToThemAs();
if (id > 0)
_outbound.remove(Long.valueOf(id));
if (peer.getTheyRelayToUsAs() > 0) {
long id2 = peer.getTheyRelayToUsAs();
if (id2 > 0) {
_inbound.remove(peer);
}
if ((id > 0 || id2 > 0) &&_log.shouldLog(Log.DEBUG))
_log.debug("removing peer " + peer.getRemoteHostId() + ", weRelayToThemAs "
+ id + ", theyRelayToUsAs " + id2);
}
private PeerState get(long id) {
@@ -196,8 +200,8 @@ class IntroductionManager {
}
// FIXME we can include all his addresses including IPv6 even if we don't support IPv6 (isValid() is false)
// but requires RelayRequest support, see below
RouterAddress ra = _transport.getTargetAddress(ri);
if (ra == null) {
List<RouterAddress> ras = _transport.getTargetAddresses(ri);
if (ras.isEmpty()) {
if (_log.shouldLog(Log.INFO))
_log.info("Picked peer has no SSU address: " + ri);
continue;
@@ -219,21 +223,31 @@ class IntroductionManager {
_log.info("Peer is idle too long: " + cur);
continue;
}
// FIXME we can include all his addresses including IPv6 even if we don't support IPv6 (isValid() is false)
// but requires RelayRequest support, see below
byte[] ip = cur.getRemoteIP();
int port = cur.getRemotePort();
if (!isValid(ip, port))
continue;
if (_log.shouldLog(Log.INFO))
int oldFound = found;
for (RouterAddress ra : ras) {
// IPv6 allowed as of 0.9.50
byte[] ip = ra.getIP();
if (ip.length == 16 && VersionComparator.comp(ri.getVersion(), MIN_IPV6_INTRODUCER_VERSION) < 0) {
if (_log.shouldLog(Log.INFO))
_log.info("Would have picked IPv6 introducer but he doesn't support it: " + cur);
continue;
}
int port = ra.getPort();
if (!isValid(ip, port, true))
continue;
cur.setIntroducerTime();
UDPAddress ura = new UDPAddress(ra);
byte[] ikey = ura.getIntroKey();
if (ikey == null)
continue;
introducers.add(new Introducer(ip, port, ikey, cur.getTheyRelayToUsAs()));
found++;
// two per router max
if (found - oldFound >= 2)
break;
}
if (oldFound != found && _log.shouldLog(Log.INFO))
_log.info("Picking introducer: " + cur);
cur.setIntroducerTime();
UDPAddress ura = new UDPAddress(ra);
byte[] ikey = ura.getIntroKey();
if (ikey == null)
continue;
introducers.add(new Introducer(ip, port, ikey, cur.getTheyRelayToUsAs()));
found++;
}
// we sort them so a change in order only won't happen, and won't cause a republish
@@ -468,41 +482,58 @@ class IntroductionManager {
int ipSize = rrReader.readIPSize();
int port = rrReader.readPort();
// ip/port inside message should be 0:0, as it's unimplemented on send -
// see PacketBuilder.buildRelayRequest()
// and we don't read it here.
// FIXME implement for getting Alice's IPv4 in RelayRequest sent over IPv6?
// or is that just too easy to spoof?
byte[] aliceIP = alice.getIP();
int alicePort = alice.getPort();
if (!isValid(alice.getIP(), alice.getPort())) {
boolean ipIncluded = ipSize != 0;
// here we allow IPv6, but only if there's an IP included
if (!isValid(aliceIP, alicePort, ipIncluded)) {
// not necessarily invalid ip/port, could be blocklisted
if (_log.shouldWarn())
_log.warn("Bad relay req from " + alice + " for " + Addresses.toString(aliceIP, alicePort));
_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
// TODO relay request over IPv6
if (ipSize != 0) {
// 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 (!Arrays.equals(aliceIP, ip)) {
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
// TODO relay request over IPv6
if (port != 0 && port != alicePort) {
if (_log.shouldWarn())
_log.warn("Bad relay req from " + alice + " for " + Addresses.toString(aliceIP, port));
_context.statManager().addRateData("udp.relayBadIP", 1);
// 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
// here we do not allow IPv6
RemoteHostId aliceRelayID;
if (ipIncluded) {
if (!isValid(aliceIP, alicePort)) {
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) {
@@ -522,7 +553,7 @@ class IntroductionManager {
_context.statManager().addRateData("udp.receiveRelayRequest", 1);
// send that peer an introduction for alice
_transport.send(_builder.buildRelayIntro(alice, charlie, reader.getRelayRequestReader()));
_transport.send(_builder.buildRelayIntro(aliceRelayID, charlie, rrReader));
// send alice back charlie's info
// lookup session so we can use session key if available
@@ -546,7 +577,7 @@ class IntroductionManager {
if (_log.shouldLog(Log.INFO))
_log.info("Sending relay response (in-session) to " + alice);
}
_transport.send(_builder.buildRelayResponse(alice, charlie, reader.getRelayRequestReader().readNonce(),
_transport.send(_builder.buildRelayResponse(alice, charlie, rrReader.readNonce(),
cipherKey, macKey));
}
@@ -557,8 +588,17 @@ class IntroductionManager {
* @since 0.9.3
*/
private boolean isValid(byte[] ip, int port) {
return isValid(ip, port, false);
}
/**
* Are IP and port valid?
* @since 0.9.50
*/
private boolean isValid(byte[] ip, int port, boolean allowIPv6) {
return TransportUtil.isValidPort(port) &&
ip != null && ip.length == 4 &&
ip != null &&
(ip.length == 4 || (allowIPv6 && ip.length == 16)) &&
_transport.isValid(ip) &&
(!_transport.isTooClose(ip)) &&
(!_context.blocklist().isBlocklisted(ip));

View File

@@ -2,6 +2,7 @@ package net.i2p.router.transport.udp;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -16,6 +17,7 @@ import net.i2p.data.Hash;
import net.i2p.data.router.RouterIdentity;
import net.i2p.data.SessionKey;
import net.i2p.data.Signature;
import net.i2p.data.router.RouterAddress;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.TransportUtil;
import net.i2p.util.Addresses;
@@ -1193,12 +1195,6 @@ class PacketBuilder {
return packet;
}
// specify these if we know what our external receive ip/port is and if its different
// from what bob is going to think
// FIXME IPv4 addr must be specified when sent over IPv6
private byte[] getOurExplicitIP() { return null; }
private int getOurExplicitPort() { return 0; }
/**
* build intro packets for each of the published introducers
*
@@ -1228,7 +1224,7 @@ class PacketBuilder {
(Arrays.equals(iaddr.getAddress(), _transport.getExternalIP()) && !_transport.allowLocal())) {
if (_log.shouldLog(Log.WARN))
_log.warn("Cannot build a relay request for " + state.getRemoteIdentity().calculateHash()
+ ", as the introducer address is invalid: " + iaddr + ':' + iport);
+ ", as the introducer address is invalid: " + Addresses.toString(iaddr.getAddress(), iport));
// TODO implement some sort of introducer banlist
continue;
}
@@ -1255,22 +1251,24 @@ class PacketBuilder {
cipherKey = new SessionKey(ikey);
macKey = cipherKey;
if (_log.shouldLog(Log.INFO))
_log.info("Sending relay request (w/ intro key) to " + iaddr + ":" + iport);
_log.info("Sending relay request (w/ intro key) to " + Addresses.toString(iaddr.getAddress(), iport));
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Sending relay request (in-session) to " + iaddr + ":" + iport);
_log.info("Sending relay request (in-session) to " + Addresses.toString(iaddr.getAddress(), iport));
}
rv.add(buildRelayRequest(iaddr, iport, cipherKey, macKey, tag,
ourIntroKey, state.getIntroNonce()));
UDPPacket pkt = buildRelayRequest(iaddr, iport, cipherKey, macKey, tag, ourIntroKey, state.getIntroNonce());
if (pkt != null)
rv.add(pkt);
else if (_log.shouldWarn())
_log.warn("Cannot build a relay request for " + state.getRemoteIdentity().calculateHash()
+ ", as we don't have an IPv4 address to send to: " + Addresses.toString(iaddr.getAddress(), iport));
}
return rv;
}
/**
* TODO Alice IP/port in packet will always be null/0, must be fixed to
* send a RelayRequest over IPv6
*
* @return null on failure
*/
private UDPPacket buildRelayRequest(InetAddress introHost, int introPort,
SessionKey cipherKey, SessionKey macKey,
@@ -1280,9 +1278,21 @@ class PacketBuilder {
byte data[] = pkt.getData();
int off = HEADER_SIZE;
// FIXME must specify these if request is going over IPv6
byte ourIP[] = getOurExplicitIP();
int ourPort = getOurExplicitPort();
// Must specify these if request is going over IPv6
byte ourIP[];
int ourPort;
if (introHost instanceof Inet6Address) {
RouterAddress ra = _transport.getCurrentExternalAddress(false);
if (ra == null)
return null;
ourIP = ra.getIP();
if (ourIP == null)
return null;
ourPort = _transport.getRequestedPort();
} else {
ourIP = null;
ourPort = 0;
}
// now for the body
DataHelper.toLong(data, off, 4, introTag);

View File

@@ -2637,9 +2637,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
* we don't put them in the real, published RouterAddress anymore
* if we are firewalled.
*
* @since 0.9.18
* @since 0.9.18, pkg private for PacketBuilder since 0.9.50
*/
private RouterAddress getCurrentExternalAddress(boolean isIPv6) {
RouterAddress getCurrentExternalAddress(boolean isIPv6) {
// deadlock thru here ticket #1699
synchronized (_rebuildLock) {
return isIPv6 ? _currentOurV6Address : _currentOurV4Address;
@@ -2717,6 +2717,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
/**
* Do we require introducers?
* Currently for IPv4 only.
*/
public boolean introducersRequired() {
/******************
@@ -2755,6 +2756,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
* MIGHT we require introducers?
* This is like introducersRequired, but if we aren't sure, this returns true.
* Used only by EstablishmentManager.
* Currently for IPv4 only.
*
* @since 0.9.24
*/
@@ -2776,7 +2778,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
/**
* For EstablishmentManager
* For EstablishmentManager.
* Currently for IPv4 only.
* @since 0.9.3
*/
boolean canIntroduce() {
@@ -2789,6 +2792,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
(!introducersRequired()) &&
haveCapacity() &&
(!_context.netDb().floodfillEnabled()) &&
getIPv6Config() != IPV6_ONLY &&
_introManager.introducedCount() < IntroductionManager.MAX_OUTBOUND &&
_introManager.introducedCount() < getMaxConnections() / 4;
}