Send clientinit without waiting for clientek's ACK
The server does not acknowledge a resent clientek, so losing its first ACK stalled the handshake: we kept resending clientek and never got to clientinit. Send clientek without blocking (resends continue in the background via the new writePacketAsync) and follow with clientinit right away; the server cannot decrypt clientinit before it has processed clientek, so the two stay in order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -185,7 +185,9 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
|||||||
getClient().getIdentity()
|
getClient().getIdentity()
|
||||||
);
|
);
|
||||||
|
|
||||||
getClient().writePacket(new PacketBody2Command(
|
// Don't wait for the ACK: the server ignores a resent clientek, so a lost ACK would stall us, and
|
||||||
|
// it can't decrypt clientinit before processing clientek anyway, so the two stay in order.
|
||||||
|
getClient().writePacketAsync(new PacketBody2Command(
|
||||||
ProtocolRole.CLIENT,
|
ProtocolRole.CLIENT,
|
||||||
new SingleCommand(
|
new SingleCommand(
|
||||||
"clientek", ProtocolRole.CLIENT,
|
"clientek", ProtocolRole.CLIENT,
|
||||||
|
|||||||
+74
-18
@@ -283,6 +283,41 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
||||||
|
writePacket(newOutgoingPacket(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a packet body without waiting for its acknowledgement. It is numbered and encrypted before this
|
||||||
|
* returns, so packets written afterwards follow it; resends until it is acknowledged continue in the background.
|
||||||
|
*
|
||||||
|
* @return completes once the packet is acknowledged, or exceptionally if it never is
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> writePacketAsync(PacketBody body) throws IOException, TimeoutException {
|
||||||
|
Packet packet = newOutgoingPacket(body);
|
||||||
|
List<PacketResponse> responses = new ArrayList<>();
|
||||||
|
|
||||||
|
synchronized (sendLocks.get(packet.getHeader().getType())) {
|
||||||
|
for (Packet piece : split(packet))
|
||||||
|
responses.add(sendPacketIntl(piece));
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Void> acknowledged = new CompletableFuture<>();
|
||||||
|
Thread thread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
for (PacketResponse response : responses)
|
||||||
|
awaitAcknowledgement(response);
|
||||||
|
acknowledged.complete(null);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
acknowledged.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
}, "TS3J/Resend");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
|
||||||
|
return acknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Packet newOutgoingPacket(PacketBody body) {
|
||||||
// Construct a packet object
|
// Construct a packet object
|
||||||
Packet packet = new Packet(body.getRole());
|
Packet packet = new Packet(body.getRole());
|
||||||
|
|
||||||
@@ -318,12 +353,32 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
// Assign header pointer
|
// Assign header pointer
|
||||||
packet.setHeader(header);
|
packet.setHeader(header);
|
||||||
|
|
||||||
// Lower-level write; helper is done
|
return packet;
|
||||||
writePacket(packet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writePacket(Packet packet) throws IOException, TimeoutException {
|
public void writePacket(Packet packet) throws IOException, TimeoutException {
|
||||||
|
List<Packet> pieces = split(packet);
|
||||||
|
|
||||||
|
if (packet.getHeader().getType().isSplittable()) {
|
||||||
|
// Note, it's very important we send these packets synchronously.
|
||||||
|
// we allow the split to happen multi-thread, but we block until all
|
||||||
|
// chunks are sent synchronously. So, all chunks of packets are synchronous.
|
||||||
|
// this happens because we care that the packet IDs are contiguous.
|
||||||
|
synchronized (sendLocks.get(packet.getHeader().getType())) {
|
||||||
|
for (Packet piece : pieces)
|
||||||
|
awaitAcknowledgement(sendPacketIntl(piece)); // Write the individual piece, incrementing its packet
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Don't lock here, for the slight performance benefit.
|
||||||
|
awaitAcknowledgement(sendPacketIntl(packet));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks an outgoing packet and splits it into the pieces to send.
|
||||||
|
*/
|
||||||
|
private List<Packet> split(Packet packet) throws IOException {
|
||||||
// Sanity checks. People are insane!
|
// Sanity checks. People are insane!
|
||||||
if (packet.getHeader() == null)
|
if (packet.getHeader() == null)
|
||||||
throw new NullPointerException("header");
|
throw new NullPointerException("header");
|
||||||
@@ -339,28 +394,23 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
// it's better to do this all at once instead of sending them as they are split,
|
// it's better to do this all at once instead of sending them as they are split,
|
||||||
// because we might get halfway through splitting them then throw an exception and
|
// because we might get halfway through splitting them then throw an exception and
|
||||||
// we can't write the rest.
|
// we can't write the rest.
|
||||||
List<Packet> packets = Fragments.split(packet);
|
return Fragments.split(packet);
|
||||||
|
|
||||||
// Note, it's very important we send these packets synchronously.
|
|
||||||
// we allow the split to happen multi-thread, but we block until all
|
|
||||||
// chunks are sent synchronously. So, all chunks of packets are synchronous.
|
|
||||||
// this happens because we care that the packet IDs are contiguous.
|
|
||||||
synchronized (sendLocks.get(packet.getHeader().getType())) {
|
|
||||||
for (Packet piece : packets)
|
|
||||||
writePacketIntl(piece); // Write the individual piece, incrementing its packet
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Ensure the outbound packet's maximum size isn't too large if it can't be split (which it can't here)
|
// Ensure the outbound packet's maximum size isn't too large if it can't be split (which it can't here)
|
||||||
if (packet.getSize() > Fragments.MAXIMUM_PACKET_SIZE)
|
if (packet.getSize() > Fragments.MAXIMUM_PACKET_SIZE)
|
||||||
throw new IllegalArgumentException("packet too large: " + packet.getSize() + "; " +
|
throw new IllegalArgumentException("packet too large: " + packet.getSize() + "; " +
|
||||||
packet.getHeader().getType().name() + " cannot split");
|
packet.getHeader().getType().name() + " cannot split");
|
||||||
|
|
||||||
// Don't lock here, for the slight performance benefit.
|
return Collections.singletonList(packet);
|
||||||
writePacketIntl(packet);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writePacketIntl(Packet packet) throws IOException, TimeoutException {
|
/**
|
||||||
|
* Numbers, encrypts and sends a single packet.
|
||||||
|
*
|
||||||
|
* @return the pending acknowledgement to wait for, or null if none is expected
|
||||||
|
*/
|
||||||
|
private PacketResponse sendPacketIntl(Packet packet) throws IOException {
|
||||||
PacketHeader header = packet.getHeader();
|
PacketHeader header = packet.getHeader();
|
||||||
|
|
||||||
// Count packet
|
// Count packet
|
||||||
@@ -486,8 +536,14 @@ public abstract class AbstractTeamspeakClientSocket
|
|||||||
.get(kind)
|
.get(kind)
|
||||||
.processOutgoing(packet);
|
.processOutgoing(packet);
|
||||||
|
|
||||||
// If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException
|
return willWaitForAck ? response : null;
|
||||||
if (response != null && willWaitForAck) {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a packet's acknowledgement, resending it until it arrives.
|
||||||
|
*/
|
||||||
|
private void awaitAcknowledgement(PacketResponse response) throws IOException, TimeoutException {
|
||||||
|
if (response != null) {
|
||||||
while (response.getRetries() < response.getMaxTries() &&
|
while (response.getRetries() < response.getMaxTries() &&
|
||||||
getState() != ClientConnectionState.DISCONNECTED) {
|
getState() != ClientConnectionState.DISCONNECTED) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user