SSU2: Add per-introducer relay state machine

Enable relay
Set session created token to 0 as per spec
Log tweaks
This commit is contained in:
zzz
2022-06-14 12:48:00 -04:00
parent c248279a03
commit 0a87559ba2
7 changed files with 227 additions and 40 deletions

View File

@@ -1,3 +1,13 @@
2022-06-14 zzz
* SSU2:
- Add per-introducer relay state machine
- Enable relay
2022-06-12 zzz
* SSU2:
- Fix peer test msg 1 signature
- Relay fixes
2022-06-11 zzz
* SSU: Don't send SSU1 relay request to SSU2 peer

View File

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

View File

@@ -33,6 +33,7 @@ import net.i2p.router.transport.TransportUtil;
import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
import static net.i2p.router.transport.udp.InboundEstablishState.InboundState.*;
import static net.i2p.router.transport.udp.OutboundEstablishState.OutboundState.*;
import static net.i2p.router.transport.udp.OutboundEstablishState2.IntroState.*;
import static net.i2p.router.transport.udp.SSU2Util.*;
import net.i2p.router.util.DecayingHashSet;
import net.i2p.router.util.DecayingBloomFilter;
@@ -1340,23 +1341,49 @@ class EstablishmentManager {
_log.debug("Send relay request for " + state + " with our intro key as " + _transport.getIntroKey());
state.introSent();
} else {
// walk through the state machine for each SSU2 introducer
OutboundEstablishState2 state2 = (OutboundEstablishState2) state;
// establish() above ensured there is at least one valid v2 introducer
// Look for a connected peer, if found, use the first one only.
long now = _context.clock().now();
UDPAddress addr = state.getRemoteAddress();
int count = addr.getIntroducerCount();
for (int i = 0; i < count; i++) {
Hash h = addr.getIntroducerHash(i);
long exp = addr.getIntroducerExpiration(i);
if (h != null && (exp > now || exp == 0)) {
PeerState bob = _transport.getPeerState(h);
// TODO cross-version relaying, maybe
if (bob != null && bob.getVersion() == 2) {
if (h != null) {
PeerState bob = null;
OutboundEstablishState2.IntroState istate = state2.getIntroState(h);
switch (istate) {
case INTRO_STATE_INIT:
case INTRO_STATE_CONNECTING:
bob = _transport.getPeerState(h);
if (bob != null) {
if (bob.getVersion() == 2) {
istate = INTRO_STATE_CONNECTED;
state2.setIntroState(h, istate);
} else {
// TODO cross-version relaying, maybe
istate = INTRO_STATE_REJECTED;
state2.setIntroState(h, istate);
}
}
break;
case INTRO_STATE_CONNECTED:
bob = _transport.getPeerState(h);
if (bob == null) {
istate = INTRO_STATE_DISCONNECTED;
state2.setIntroState(h, istate);
}
break;
}
if (bob != null && istate == INTRO_STATE_CONNECTED) {
if (_log.shouldDebug())
_log.debug("Found connected introducer " + bob + " for " + state);
long tag = addr.getIntroducerTag(i);
sendRelayRequest(tag, (PeerState2) bob, state);
state.introSent();
// this transitions the state
state2.introSent(h);
return;
}
}
@@ -1365,19 +1392,20 @@ class EstablishmentManager {
boolean sent = false;
for (int i = 0; i < count; i++) {
Hash h = addr.getIntroducerHash(i);
long exp = addr.getIntroducerExpiration(i);
if (h != null && (exp > now || exp == 0)) {
if (_context.banlist().isBanlisted(h))
continue;
PeerState bobState = _transport.getPeerState(h);
if (bobState != null) {
// presumably SSU1 or we would have used it above
if (_log.shouldDebug())
_log.debug("Skipping SSU1-connected introducer " + bobState + " for " + state);
continue;
if (h != null) {
RouterInfo bob = null;
OutboundEstablishState2.IntroState istate = state2.getIntroState(h);
OutboundEstablishState2.IntroState oldState = istate;
switch (istate) {
case INTRO_STATE_INIT:
case INTRO_STATE_LOOKUP_SENT:
case INTRO_STATE_HAS_RI:
bob = _context.netDb().lookupRouterInfoLocally(h);
if (bob != null)
istate = INTRO_STATE_HAS_RI;
break;
}
RouterInfo bob = _context.netDb().lookupRouterInfoLocally(h);
if (bob != null) {
if (bob != null && istate == INTRO_STATE_HAS_RI) {
List<RouterAddress> addrs = _transport.getTargetAddresses(bob);
for (RouterAddress ra : addrs) {
byte[] ip = ra.getIP();
@@ -1399,16 +1427,23 @@ class EstablishmentManager {
DatabaseLookupMessage dlm = new DatabaseLookupMessage(_context);
dlm.setSearchKey(h);
dlm.setSearchType(DatabaseLookupMessage.Type.RI);
long now = _context.clock().now();
dlm.setMessageExpiration(now + 10*1000);
dlm.setFrom(_context.routerHash());
OutNetMessage m = new OutNetMessage(_context, dlm, now + 10*1000, OutNetMessage.PRIORITY_MY_NETDB_LOOKUP, bob);
establish(m);
istate = INTRO_STATE_CONNECTING;
// for now, just wait until this method is called again,
// hopefully somebody has connected
break;
}
}
}
// if we didn't try to connect, it must have had a bad RI
if (istate == INTRO_STATE_HAS_RI)
istate = INTRO_STATE_REJECTED;
if (oldState != istate)
state2.setIntroState(h, istate);
}
}
if (sent) {
@@ -1419,15 +1454,17 @@ class EstablishmentManager {
// Otherwise, look up the RIs first.
for (int i = 0; i < count; i++) {
Hash h = addr.getIntroducerHash(i);
long exp = addr.getIntroducerExpiration(i);
if (h != null && (exp > now || exp == 0)) {
if (_context.banlist().isBanlisted(h))
continue;
if (_log.shouldDebug())
_log.debug("Looking up introducer " + h + " for " + state);
// TODO on success job
_context.netDb().lookupRouterInfo(h, null, null, 10*1000);
sent = true;
if (h != null) {
OutboundEstablishState2.IntroState istate = state2.getIntroState(h);
if (istate == INTRO_STATE_INIT) {
if (_log.shouldDebug())
_log.debug("Looking up introducer " + h + " for " + state);
istate = INTRO_STATE_LOOKUP_SENT;
state2.setIntroState(h, istate);
// TODO on success job
_context.netDb().lookupRouterInfo(h, null, null, 10*1000);
sent = true;
}
}
}
if (sent) {
@@ -1569,6 +1606,9 @@ class EstablishmentManager {
_log.debug("Dup or unknown RelayResponse: " + nonce);
return; // already established
}
if (charlie.getVersion() != 2)
return;
OutboundEstablishState2 charlie2 = (OutboundEstablishState2) charlie;
long token;
if (code == 0) {
token = DataHelper.fromLong8(data, data.length - 8);
@@ -1581,10 +1621,17 @@ class EstablishmentManager {
RouterInfo bobRI = _context.netDb().lookupRouterInfoLocally(bobHash);
RouterInfo charlieRI = _context.netDb().lookupRouterInfoLocally(charlieHash);
Hash signer;
if (code > 0 && code < 64)
OutboundEstablishState2.IntroState istate;
if (code > 0 && code < 64) {
signer = bobHash;
else
istate = INTRO_STATE_BOB_REJECT;
} else {
signer = charlieHash;
if (code == 0)
istate = INTRO_STATE_SUCCESS;
else
istate = INTRO_STATE_CHARLIE_REJECT;
}
RouterInfo signerRI = _context.netDb().lookupRouterInfoLocally(signer);
if (signerRI != null) {
// validate signed data
@@ -1594,6 +1641,7 @@ class EstablishmentManager {
} else {
if (_log.shouldWarn())
_log.warn("Signature failed relay response " + code + " as alice from:\n" + signerRI);
istate = INTRO_STATE_FAILED;
}
} else {
if (_log.shouldWarn())
@@ -1604,6 +1652,8 @@ class EstablishmentManager {
if (iplen != 6 && iplen != 18) {
if (_log.shouldWarn())
_log.warn("Bad IP length " + iplen + " from " + charlie);
istate = INTRO_STATE_FAILED;
charlie2.setIntroState(bobHash, istate);
charlie.fail();
return;
}
@@ -1619,6 +1669,8 @@ class EstablishmentManager {
if (_log.shouldLog(Log.WARN))
_log.warn("Bad relay resp from " + charlie + " for " + Addresses.toString(ip, port));
_context.statManager().addRateData("udp.relayBadIP", 1);
istate = INTRO_STATE_FAILED;
charlie2.setIntroState(bobHash, istate);
charlie.fail();
return;
}
@@ -1626,6 +1678,8 @@ class EstablishmentManager {
try {
charlieIP = InetAddress.getByAddress(ip);
} catch (UnknownHostException uhe) {
istate = INTRO_STATE_FAILED;
charlie2.setIntroState(bobHash, istate);
charlie.fail();
return;
}
@@ -1635,6 +1689,7 @@ class EstablishmentManager {
if (charlieRI == null) {
if (_log.shouldWarn())
_log.warn("Charlie RI not found " + charlie);
charlie2.setIntroState(bobHash, istate);
// maybe it will show up later
return;
}
@@ -1659,11 +1714,13 @@ class EstablishmentManager {
if (claimed != null)
_outboundByClaimedAddress.remove(oldId, charlie); // only if == state
}
charlie2.setIntroState(bobHash, istate);
notifyActivity();
} else if (code >= 64) {
// that's it
if (_log.shouldDebug())
_log.debug("Received RelayResponse rejection " + code + " from charlie " + charlie);
charlie2.setIntroState(bobHash, istate);
charlie.fail();
_liveIntroductions.remove(lnonce);
} else {
@@ -1671,6 +1728,7 @@ class EstablishmentManager {
// TODO keep track
if (_log.shouldDebug())
_log.debug("Received RelayResponse rejection " + code + " from bob " + bob);
charlie2.setIntroState(bobHash, istate);
notifyActivity();
}
}

View File

@@ -6,7 +6,9 @@ import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.southernstorm.noise.protocol.ChaChaPolyCipherState;
import com.southernstorm.noise.protocol.CipherState;
@@ -43,6 +45,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
private final long _sendConnID;
private final long _rcvConnID;
private final RouterAddress _routerAddress;
private final Map<Hash, IntroState> _introducers;
private long _token;
private HandshakeState _handshakeState;
private final byte[] _sendHeaderEncryptKey1;
@@ -61,6 +64,58 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
private static final boolean SET_TOKEN = false;
private static final long MAX_SKEW = 2*60*1000L;
/**
* Per-introducer introduction states
* @since 0.9.55
*/
public enum IntroState {
// pending states
// we may transition from these to another state
// See EstablishmentManager.handlePendingIntro() for state machine
/** nothing happened yet */
INTRO_STATE_INIT,
/** lookup for the introducer RI was sent */
INTRO_STATE_LOOKUP_SENT,
/** we have the introducer RI */
INTRO_STATE_HAS_RI,
/** we are connecting to the introducer */
INTRO_STATE_CONNECTING,
/** we are connected to this introducer */
INTRO_STATE_CONNECTED,
/** we sent the relay request to this introducer */
INTRO_STATE_RELAY_REQUEST_SENT,
/** we got a good relay response via this introducer */
INTRO_STATE_RELAY_CHARLIE_ACCEPTED,
// final states
// we do not transition from these states
/** introducer has expired */
INTRO_STATE_EXPIRED,
/** we tried to lookup the introducer RI, no luck */
INTRO_STATE_LOOKUP_FAILED,
/** we rejected this introducer for some reason */
INTRO_STATE_REJECTED,
/** we failed to connect to the introducer */
INTRO_STATE_CONNECT_FAILED,
/** he disconnected from us along the way */
INTRO_STATE_DISCONNECTED,
/** we failed to get a relay response from this introducer */
INTRO_STATE_RELAY_RESPONSE_TIMEOUT,
/** we got a rejection from this introducer */
INTRO_STATE_BOB_REJECT,
/** we got a rejection from Charlie via this introducer */
INTRO_STATE_CHARLIE_REJECT,
/** unspecified failure */
INTRO_STATE_FAILED,
/** this peer is not an introducer */
INTRO_STATE_INVALID,
/** we got an accept from Charlie via this introducer */
INTRO_STATE_SUCCESS
}
/**
* Prepare to start a new handshake with the given peer.
*
@@ -112,9 +167,27 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
}
_mtu = mtu;
_routerAddress = ra;
if (addr.getIntroducerCount() > 0) {
int intros = addr.getIntroducerCount();
if (intros > 0) {
_currentState = OutboundState.OB_STATE_PENDING_INTRO;
// we will get a token in the relay response or hole punch
_introducers = new HashMap<Hash, IntroState>(4);
// Initial setup of per-introducer state tracking.
// See EstablishmentManager.handlePendingIntro() for state machine
for (int i = 0; i < intros; i++) {
Hash h = addr.getIntroducerHash(i);
if (h != null) {
IntroState istate;
long exp = addr.getIntroducerExpiration(i);
if (exp != 0 && exp < _establishBegin)
istate = IntroState.INTRO_STATE_EXPIRED;
else if (_context.banlist().isBanlisted(h))
istate = IntroState.INTRO_STATE_REJECTED;
else
istate = IntroState.INTRO_STATE_INIT;
_introducers.put(h, istate);
}
}
} else {
_token = _transport.getEstablisher().getOutboundToken(_remoteHostId);
if (_token != 0) {
@@ -123,6 +196,7 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
} else {
_currentState = OutboundState.OB_STATE_NEEDS_TOKEN;
}
_introducers = null;
}
_sendConnID = ctx.random().nextLong();
@@ -562,12 +636,56 @@ class OutboundEstablishState2 extends OutboundEstablishState implements SSU2Payl
return _pstate;
}
/**
* @return non-null current state for the SSU2 introducer specified,
* or INTRO_STATE_INVALID if peer is not an SSU2 introducer
* @since 0.9.55
*/
public IntroState getIntroState(Hash h) {
IntroState rv;
if (_introducers == null) {
rv = IntroState.INTRO_STATE_INVALID;
} else {
synchronized(_introducers) {
rv = _introducers.get(h);
}
if (rv == null)
rv = IntroState.INTRO_STATE_INVALID;
}
return rv;
}
/**
* Set the current state for the SSU2 introducer specified
* @since 0.9.55
*/
public void setIntroState(Hash h, IntroState state) {
if (_introducers == null)
return;
IntroState old;
synchronized(_introducers) {
old = _introducers.put(h, state);
}
if (_log.shouldDebug())
_log.debug("Change state for introducer " + h.toBase64() + " from " + old + " to " + state + " on " + this);
}
/**
* A relay request was sent to the SSU2 introducer specified
* @since 0.9.55
*/
public void introSent(Hash h) {
setIntroState(h, IntroState.INTRO_STATE_RELAY_REQUEST_SENT);
introSent();
}
@Override
public String toString() {
return "OES2 " + _remotePeer.getHash().toBase64().substring(0, 6) + ' ' + _remoteHostId +
" lifetime: " + DataHelper.formatDuration(getLifetime()) +
" Rcv ID: " + _rcvConnID +
" Send ID: " + _sendConnID +
' ' + _currentState;
' ' + _currentState +
(_introducers != null ? (" Introducers: " + _introducers.toString()) : "");
}
}

View File

@@ -398,7 +398,7 @@ class PacketBuilder2 {
public UDPPacket buildSessionCreatedPacket(InboundEstablishState2 state) {
long n = _context.random().signedNextInt() & 0xFFFFFFFFL;
UDPPacket packet = buildLongPacketHeader(state.getSendConnID(), n, SESSION_CREATED_FLAG_BYTE,
state.getRcvConnID(), state.getToken());
state.getRcvConnID(), 0);
DatagramPacket pkt = packet.getPacket();
byte sentIP[] = state.getSentIP();
@@ -937,10 +937,11 @@ class PacketBuilder2 {
}
/**
* Also used for hole punch with a relay request block.
* Also used for retry with ptBlock = null
*
* @param packet containing only 32 byte header
* @param ptBlock null for retry
* @param ptBlock Peer Test or Relay Request block. Null for retry.
*/
private void encryptPeerTest(UDPPacket packet, byte[] chachaKey, long n,
byte[] hdrKey1, byte[] hdrKey2, byte[] ip, int port,
@@ -975,12 +976,12 @@ class PacketBuilder2 {
pkt.setLength(pkt.getLength() + len + MAC_LEN);
} catch (RuntimeException re) {
if (!_log.shouldWarn())
_log.error("Bad retry/test msg out", re);
_log.error("Bad retry/test/holepunch msg out", re);
throw re;
} catch (GeneralSecurityException gse) {
if (!_log.shouldWarn())
_log.error("Bad retry/test msg out", gse);
throw new RuntimeException("Bad retry/test msg out", gse);
_log.error("Bad retry/test/holepunch msg out", gse);
throw new RuntimeException("Bad retry/test/holepunch msg out", gse);
}
SSU2Header.encryptLongHeader(packet, hdrKey1, hdrKey2);
}

View File

@@ -577,7 +577,7 @@ public class PeerState2 extends PeerState implements SSU2Payload.PayloadCallback
// IllegalArgumentException, buggy ack block, let the other blocks get processed
if (_log.shouldWarn())
_log.warn("Bad ACK block\n" + SSU2Bitfield.toString(ackThru, acks, ranges, (ranges != null ? ranges.length / 2 : 0)) +
"\nAck through " + ackThru + " acnt " + acks + (ranges != null ? "Ranges:\n" + HexDump.dump(ranges) : "") +
"\nAck through " + ackThru + " acnt " + acks + (ranges != null ? " Ranges:\n" + HexDump.dump(ranges) : "") +
"from " + this, e);
}
}

View File

@@ -19,7 +19,7 @@ final class SSU2Util {
public static final int PROTOCOL_VERSION = 2;
// features
public static final boolean ENABLE_RELAY = false;
public static final boolean ENABLE_RELAY = true;
public static final boolean ENABLE_PEER_TEST = true;
public static final boolean ENABLE_PATH_CHALLENGE = false;