- Fail establishment immediately on SessionCreated
     validation fail
   - Defer outbound DH generation until required
   - Validate address/port in RelayIntro messages
   - Throttle hole punches
   - More cleanups
This commit is contained in:
zzz
2012-08-22 17:43:09 +00:00
parent 1d41c2fd19
commit b61127270e
8 changed files with 225 additions and 83 deletions

View File

@@ -1,3 +1,15 @@
2012-08-22 zzz
* NetDB: Add hash collision detection
* SimpleTimer2: Synchronization improvements (ticket #653)
* SSU:
- Fail establishment immediately on SessionCreated
validation fail
- Defer outbound DH generation until required
- Validate address/port in RelayIntro messages
- Throttle hole punches
- Workaround for Android ICS bug
- More cleanups
2012-08-21 zzz
* NetDB: Decrease stat publish probability
* SSU:

View File

@@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 14;
public final static long BUILD = 15;
/** for example "-test" */
public final static String EXTRA = "";

View File

@@ -340,7 +340,7 @@ class EstablishmentManager {
}
state = new OutboundEstablishState(_context, maybeTo, to,
toIdentity,
sessionKey, addr, _transport.getDHBuilder());
sessionKey, addr, _transport.getDHFactory());
OutboundEstablishState oldState = _outboundStates.putIfAbsent(to, state);
boolean isNew = oldState == null;
if (isNew) {
@@ -394,6 +394,15 @@ class EstablishmentManager {
return getMaxConcurrentEstablish()/2;
}
/**
* Should we allow another inbound establishment?
* Used to throttle outbound hole punches.
* @since 0.9.2
*/
public boolean shouldAllowInboundEstablishment() {
return _inboundStates.size() < getMaxInboundEstablishers();
}
/**
* Got a SessionRequest (initiates an inbound establishment)
*
@@ -405,15 +414,13 @@ class EstablishmentManager {
return;
}
int maxInbound = getMaxInboundEstablishers();
boolean isNew = false;
InboundEstablishState state = _inboundStates.get(from);
if (state == null) {
// TODO this is insufficient to prevent DoSing, especially if
// IP spoofing is used. For further study.
if (_inboundStates.size() >= maxInbound) {
if (!shouldAllowInboundEstablishment()) {
if (_log.shouldLog(Log.WARN))
_log.warn("Dropping inbound establish, increase " + PROP_MAX_CONCURRENT_ESTABLISH);
_context.statManager().addRateData("udp.establishDropped", 1);
@@ -861,10 +868,15 @@ class EstablishmentManager {
state.setIntroNonce(nonce);
}
_context.statManager().addRateData("udp.sendIntroRelayRequest", 1, 0);
UDPPacket requests[] = _builder.buildRelayRequest(_transport, state, _transport.getIntroKey());
for (int i = 0; i < requests.length; i++) {
if (requests[i] != null)
_transport.send(requests[i]);
List<UDPPacket> requests = _builder.buildRelayRequest(_transport, state, _transport.getIntroKey());
if (requests.isEmpty()) {
// FIXME need a failed OB state
if (_log.shouldLog(Log.WARN))
_log.warn("No valid introducers! " + state);
// set failed state, remove nonce, and return
}
for (UDPPacket req : requests) {
_transport.send(req);
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send intro for " + state + " with our intro key as " + _transport.getIntroKey());
@@ -895,8 +907,9 @@ class EstablishmentManager {
if (_log.shouldLog(Log.WARN))
_log.warn("Introducer for " + state + " (" + bob + ") sent us an invalid address for our target: " + Addresses.toString(ip, port), uhe);
// these two cause this peer to requeue for a new intro peer
state.introductionFailed();
notifyActivity();
// FIXME no it doesn't, we send to all at once
//state.introductionFailed();
//notifyActivity();
return;
}
_context.statManager().addRateData("udp.receiveIntroRelayResponse", state.getLifetime(), 0);
@@ -936,7 +949,7 @@ class EstablishmentManager {
boolean valid = state.validateSessionCreated();
if (!valid) {
// validate clears fields on failure
// TODO - send destroy? shitlist?
// sendDestroy(state) won't work as we haven't sent the confirmed...
if (_log.shouldLog(Log.WARN))
_log.warn("SessionCreated validate failed: " + state);
return;
@@ -1018,16 +1031,16 @@ class EstablishmentManager {
// completely received (though the signature may be invalid)
iter.remove();
inboundState = cur;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Removing completely confirmed inbound state");
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing completely confirmed inbound state");
break;
} else if (cur.getLifetime() > MAX_IB_ESTABLISH_TIME) {
// took too long
iter.remove();
inboundState = cur;
//_context.statManager().addRateData("udp.inboundEstablishFailedState", cur.getState(), cur.getLifetime());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Removing expired inbound state");
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing expired inbound state");
expired = true;
break;
} else if (cur.getState() == IB_STATE_FAILED) {
@@ -1133,20 +1146,19 @@ class EstablishmentManager {
for (Iterator<OutboundEstablishState> iter = _outboundStates.values().iterator(); iter.hasNext(); ) {
OutboundEstablishState cur = iter.next();
if (cur.getState() == OB_STATE_CONFIRMED_COMPLETELY) {
// completely received
OutboundEstablishState.OutboundState state = cur.getState();
if (state == OB_STATE_CONFIRMED_COMPLETELY ||
state == OB_STATE_VALIDATION_FAILED) {
iter.remove();
outboundState = cur;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Removing confirmed outbound: " + cur);
break;
} else if (cur.getLifetime() >= MAX_OB_ESTABLISH_TIME) {
// took too long
iter.remove();
outboundState = cur;
//_context.statManager().addRateData("udp.outboundEstablishFailedState", cur.getState(), cur.getLifetime());
if (_log.shouldLog(Log.DEBUG))
_log.debug("Removing expired outbound: " + cur);
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Removing expired outbound: " + cur);
break;
} else {
if (cur.getNextSendTime() <= now) {
@@ -1233,6 +1245,10 @@ class EstablishmentManager {
else if (outboundState.getNextSendTime() <= now)
handlePendingIntro(outboundState);
break;
case OB_STATE_VALIDATION_FAILED:
processExpired(outboundState);
break;
}
}

View File

@@ -178,8 +178,6 @@ class InboundEstablishState {
/** what port number do they appear to be coming from? */
public int getSentPort() { return _alicePort; }
public synchronized byte[] getBobIP() { return _bobIP; }
public synchronized byte[] getSentY() {
if (_sentY == null)
_sentY = _keyBuilder.getMyPublicValueBytes();
@@ -328,6 +326,8 @@ class InboundEstablishState {
/**
* Who is Alice (null if forged/unknown)
*
* Note that this isn't really confirmed - see below.
*/
public synchronized RouterIdentity getConfirmedIdentity() {
if (!_verificationAttempted) {
@@ -341,8 +341,13 @@ class InboundEstablishState {
* Determine if Alice sent us a valid confirmation packet. The
* identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
* + Alice's new relay key + Alice's signed on time
*
* Note that the protocol does not include a signature of the RouterIdentity,
* which could be a problem?
*
* Caller must synch on this.
*/
private synchronized void verifyIdentity() {
private void verifyIdentity() {
int identSize = 0;
for (int i = 0; i < _receivedIdentity.length; i++)
identSize += _receivedIdentity[i].length;
@@ -385,6 +390,8 @@ class InboundEstablishState {
Signature sig = new Signature(_receivedSignature);
boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
if (ok) {
// todo partial spoof detection - get peer.calculateHash(),
// lookup in netdb locally, if not equal, fail?
_receivedConfirmedIdentity = peer;
} else {
if (_log.shouldLog(Log.WARN))
@@ -408,10 +415,10 @@ class InboundEstablishState {
StringBuilder buf = new StringBuilder(128);
buf.append("IES ");
buf.append(Addresses.toString(_aliceIP, _alicePort));
if (_receivedX != null)
buf.append(" ReceivedX: ").append(Base64.encode(_receivedX, 0, 4));
if (_sentY != null)
buf.append(" SentY: ").append(Base64.encode(_sentY, 0, 4));
//if (_receivedX != null)
// buf.append(" ReceivedX: ").append(Base64.encode(_receivedX, 0, 4));
//if (_sentY != null)
// buf.append(" SentY: ").append(Base64.encode(_sentY, 0, 4));
//buf.append(" Bob: ").append(Addresses.toString(_bobIP, _bobPort));
buf.append(" RelayTag: ").append(_sentRelayTag);
//buf.append(" SignedOn: ").append(_sentSignedOnTime);

View File

@@ -1,6 +1,9 @@
package net.i2p.router.transport.udp;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -18,7 +21,7 @@ import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
/**
*
* Keep track of inbound and outbound introductions.
*/
class IntroductionManager {
private final RouterContext _context;
@@ -29,6 +32,8 @@ class IntroductionManager {
private final Map<Long, PeerState> _outbound;
/** list of peers (PeerState) who have given us introduction tags */
private final Set<PeerState> _inbound;
private final Set<InetAddress> _recentHolePunches;
private long _lastHolePunchClean;
/**
* Limit since we ping to keep the conn open
@@ -42,6 +47,11 @@ class IntroductionManager {
*/
private static final int MAX_OUTBOUND = 100;
/** Max one per target in this time */
private static final long PUNCH_CLEAN_TIME = 5*1000;
/** Max for all targets per PUNCH_CLEAN_TIME */
private static final int MAX_PUNCHES = 8;
public IntroductionManager(RouterContext ctx, UDPTransport transport) {
_context = ctx;
_log = ctx.logManager().getLog(IntroductionManager.class);
@@ -49,6 +59,7 @@ class IntroductionManager {
_builder = new PacketBuilder(ctx, transport);
_outbound = new ConcurrentHashMap(MAX_OUTBOUND);
_inbound = new ConcurrentHashSet(MAX_INBOUND);
_recentHolePunches = new HashSet(16);
ctx.statManager().createRateStat("udp.receiveRelayIntro", "How often we get a relayed request for us to talk to someone?", "udp", UDPTransport.RATES);
ctx.statManager().createRateStat("udp.receiveRelayRequest", "How often we receive a good request to relay to someone else?", "udp", UDPTransport.RATES);
ctx.statManager().createRateStat("udp.receiveRelayRequestBadTag", "Received relay requests with bad/expired tag", "udp", UDPTransport.RATES);
@@ -203,18 +214,87 @@ class IntroductionManager {
void receiveRelayIntro(RemoteHostId bob, UDPPacketReader reader) {
if (_context.router().isHidden())
return;
if (_log.shouldLog(Log.INFO))
_log.info("Receive relay intro from " + bob);
_context.statManager().addRateData("udp.receiveRelayIntro", 1, 0);
if (!_transport.allowConnection())
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();
if (_log.shouldLog(Log.INFO))
_log.info("Receive relay intro from " + bob + " for " + Addresses.toString(ip, port));
InetAddress to = null;
try {
if (!_transport.isValid(ip))
throw new UnknownHostException("non-public IP");
if (port <= 0 || port > 65535)
throw new UnknownHostException("bad port " + port);
to = InetAddress.getByAddress(ip);
} catch (UnknownHostException uhe) {
// shitlist Bob?
if (_log.shouldLog(Log.WARN))
_log.warn("IP for alice to hole punch to is invalid", uhe);
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;
}
}
// TODO throttle
// TODO IB req limits
// TODO check if already have a session or in progress state.
// 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(reader));
_transport.send(_builder.buildHolePunch(to, port));
}
/**

View File

@@ -26,10 +26,11 @@ class OutboundEstablishState {
private final RouterContext _context;
private final Log _log;
// SessionRequest message
private final byte _sentX[];
private byte _sentX[];
private byte _bobIP[];
private int _bobPort;
private final DHSessionKeyBuilder _keyBuilder;
private final DHSessionKeyBuilder.Factory _keyFactory;
private DHSessionKeyBuilder _keyBuilder;
// SessionCreated message
private byte _receivedY[];
private byte _aliceIP[];
@@ -82,7 +83,9 @@ class OutboundEstablishState {
/** we need to have someone introduce us to the peer, but haven't received a RelayResponse yet */
OB_STATE_PENDING_INTRO,
/** RelayResponse received */
OB_STATE_INTRODUCED
OB_STATE_INTRODUCED,
/** SessionConfirmed failed validation */
OB_STATE_VALIDATION_FAILED
}
/** basic delay before backoff */
@@ -99,7 +102,7 @@ class OutboundEstablishState {
public OutboundEstablishState(RouterContext ctx, RemoteHostId claimedAddress,
RemoteHostId remoteHostId,
RouterIdentity remotePeer, SessionKey introKey, UDPAddress addr,
DHSessionKeyBuilder dh) {
DHSessionKeyBuilder.Factory dh) {
_context = ctx;
_log = ctx.logManager().getLog(OutboundEstablishState.class);
if (claimedAddress != null) {
@@ -117,9 +120,7 @@ class OutboundEstablishState {
_establishBegin = ctx.clock().now();
_remoteAddress = addr;
_introductionNonce = -1;
_keyBuilder = dh;
_sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
prepareSessionRequest();
_keyFactory = dh;
if (addr.getIntroducerCount() > 0) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("new outbound establish to " + remotePeer.calculateHash() + ", with address: " + addr);
@@ -165,8 +166,10 @@ class OutboundEstablishState {
public RouterIdentity getRemoteIdentity() { return _remotePeer; }
public SessionKey getIntroKey() { return _introKey; }
/** called from constructor, no need to synch */
/** caller must synch - only call once */
private void prepareSessionRequest() {
_keyBuilder = _keyFactory.getBuilder();
_sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
byte X[] = _keyBuilder.getMyPublicValue().toByteArray();
if (X.length == 257)
System.arraycopy(X, 1, _sentX, 0, _sentX.length);
@@ -176,7 +179,13 @@ class OutboundEstablishState {
System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length);
}
public byte[] getSentX() { return _sentX; }
public synchronized byte[] getSentX() {
// We defer keygen until now so that it gets done in the Establisher loop,
// and so that we don't waste entropy on failed introductions
if (_sentX == null)
prepareSessionRequest();
return _sentX;
}
/**
* The remote side (Bob) - note that in some places he's called Charlie.
@@ -191,6 +200,11 @@ class OutboundEstablishState {
public synchronized int getSentPort() { return _bobPort; }
public synchronized void receiveSessionCreated(UDPPacketReader.SessionCreatedReader reader) {
if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) {
if (_log.shouldLog(Log.WARN))
_log.warn("Session created already failed");
return;
}
if (_receivedY != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Session created already received, ignoring");
@@ -236,6 +250,11 @@ class OutboundEstablishState {
* @return true if valid
*/
public synchronized boolean validateSessionCreated() {
if (_currentState == OutboundState.OB_STATE_VALIDATION_FAILED) {
if (_log.shouldLog(Log.WARN))
_log.warn("Session created already failed");
return false;
}
if (_receivedSignature != null) {
if (_log.shouldLog(Log.WARN))
_log.warn("Session created already validated");
@@ -265,6 +284,9 @@ class OutboundEstablishState {
}
}
/**
* The SessionCreated validation failed
*/
public synchronized void fail() {
_receivedY = null;
_aliceIP = null;
@@ -273,10 +295,9 @@ class OutboundEstablishState {
_receivedEncryptedSignature = null;
_receivedIV = null;
_receivedSignature = null;
if ( (_currentState == OutboundState.OB_STATE_UNKNOWN) ||
(_currentState == OutboundState.OB_STATE_CREATED_RECEIVED) )
_currentState = OutboundState.OB_STATE_REQUEST_SENT;
// sure, there's a chance the packet was corrupted, but in practice
// this means that Bob doesn't know his external port, so give up.
_currentState = OutboundState.OB_STATE_VALIDATION_FAILED;
_nextSend = _context.clock().now();
}
@@ -287,6 +308,8 @@ class OutboundEstablishState {
*/
private void generateSessionKey() throws DHSessionKeyBuilder.InvalidPublicParameterException {
if (_sessionKey != null) return;
if (_keyBuilder == null)
throw new DHSessionKeyBuilder.InvalidPublicParameterException("Illegal state - never generated a key builder");
_keyBuilder.setPeerPublicValue(_receivedY);
_sessionKey = _keyBuilder.getSessionKey();
ByteArray extra = _keyBuilder.getExtraBytes();

View File

@@ -2,6 +2,7 @@ package net.i2p.router.transport.udp;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
@@ -659,13 +660,15 @@ class PacketBuilder {
_log.debug("Sending request");
// now for the body
System.arraycopy(state.getSentX(), 0, data, off, state.getSentX().length);
off += state.getSentX().length;
DataHelper.toLong(data, off, 1, state.getSentIP().length);
byte[] x = state.getSentX();
System.arraycopy(x, 0, data, off, x.length);
off += x.length;
DataHelper.toLong(data, off, 1, toIP.length);
off += 1;
System.arraycopy(toIP, 0, data, off, state.getSentIP().length);
System.arraycopy(toIP, 0, data, off, toIP.length);
off += toIP.length;
DataHelper.toLong(data, off, 2, state.getSentPort());
int port = state.getSentPort();
DataHelper.toLong(data, off, 2, port);
off += 2;
// we can pad here if we want, maybe randomized?
@@ -675,7 +678,7 @@ class PacketBuilder {
off += 16 - (off % 16);
packet.getPacket().setLength(off);
authenticate(packet, state.getIntroKey(), state.getIntroKey());
setTo(packet, to, state.getSentPort());
setTo(packet, to, port);
packet.setMessageType(TYPE_SREQ);
return packet;
}
@@ -1050,14 +1053,14 @@ class PacketBuilder {
private byte[] getOurExplicitIP() { return null; }
private int getOurExplicitPort() { return 0; }
/** build intro packets for each of the published introducers */
@SuppressWarnings("static-access")
public UDPPacket[] buildRelayRequest(UDPTransport transport, OutboundEstablishState state, SessionKey ourIntroKey) {
/**
* build intro packets for each of the published introducers
* @return empty list on failure
*/
public List<UDPPacket> buildRelayRequest(UDPTransport transport, OutboundEstablishState state, SessionKey ourIntroKey) {
UDPAddress addr = state.getRemoteAddress();
int count = addr.getIntroducerCount();
if (count <= 0)
return new UDPPacket[0];
UDPPacket rv[] = new UDPPacket[count];
List<UDPPacket> rv = new ArrayList(count);
for (int i = 0; i < count; i++) {
InetAddress iaddr = addr.getIntroducerHost(i);
int iport = addr.getIntroducerPort(i);
@@ -1069,8 +1072,9 @@ class PacketBuilder {
+ ", as their UDP address is invalid: addr=" + addr + " index=" + i);
continue;
}
// TODO implement some sort of introducer shitlist
if (transport.isValid(iaddr.getAddress()))
rv[i] = buildRelayRequest(iaddr, iport, ikey, tag, ourIntroKey, state.getIntroNonce(), true);
rv.add(buildRelayRequest(iaddr, iport, ikey, tag, ourIntroKey, state.getIntroNonce(), true));
}
return rv;
}
@@ -1227,28 +1231,11 @@ class PacketBuilder {
}
/**
* Sends an empty unauthenticated packet for hole punching
* Sends an empty unauthenticated packet for hole punching.
* Parameters must be validated previously.
*/
public UDPPacket buildHolePunch(UDPPacketReader reader) {
public UDPPacket buildHolePunch(InetAddress to, int port) {
UDPPacket packet = UDPPacket.acquire(_context, false);
byte data[] = packet.getPacket().getData();
Arrays.fill(data, 0, data.length, (byte)0x0);
int ipSize = reader.getRelayIntroReader().readIPSize();
byte ip[] = new byte[ipSize];
reader.getRelayIntroReader().readIP(ip, 0);
int port = reader.getRelayIntroReader().readPort();
InetAddress to = null;
try {
to = InetAddress.getByAddress(ip);
} catch (UnknownHostException uhe) {
if (_log.shouldLog(Log.WARN))
_log.warn("IP for alice to hole punch to is invalid", uhe);
packet.release();
return null;
}
if (_log.shouldLog(Log.INFO))
_log.info("Sending relay hole punch to " + to + ":" + port);

View File

@@ -716,6 +716,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
return _peersByIdent.get(remotePeer);
}
/**
* For IntroductionManager
* @return may be null if not started
* @since 0.9.2
*/
EstablishmentManager getEstablisher() {
return _establisher;
}
/**
* Intercept RouterInfo entries received directly from a peer to inject them into
* the PeersByCapacity listing.
@@ -1682,11 +1690,20 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
}
/**
* @return a new DHSessionKeyBuilder
* @since 0.9
*/
DHSessionKeyBuilder getDHBuilder() {
return _dhFactory.getBuilder();
}
/**
* @return the factory
* @since 0.9.2
*/
DHSessionKeyBuilder.Factory getDHFactory() {
return _dhFactory;
}
private static final int FLAG_ALPHA = 0;
private static final int FLAG_IDLE_IN = 1;