feat(net): live ACE handshake verified — ConnectRequest received and parsed (Phase 4.6a/b/c/d)
Reaches the first major milestone of Phase 4: acdream's codec is proven
byte-compatible with a live ACE server. LiveHandshakeTests drives a real
UDP exchange against 127.0.0.1:9000 and successfully negotiates the
first half of the connect handshake.
Added:
- Packets/PacketHeaderOptional.cs: new ConnectRequest flag branch.
ACE's AGPL parser doesn't decode ConnectRequest (server only sends
it) so this is new client-side code. Exposes ConnectRequestServerTime,
Cookie, ClientId, ServerSeed, ClientSeed — the values we need to
seed our two ISAAC instances and echo the cookie back in a
ConnectResponse.
- NetClient.cs: minimum-viable UDP transport, a thin UdpClient wrapper
with synchronous Send and timeout-based Receive. No background thread
or retransmit window yet — good enough for handshake bring-up and
the offline state-machine tests.
- LiveHandshakeTests.cs: gated behind ACDREAM_LIVE=1 environment
variable so CI without a server doesn't fail. Reads credentials
from ACDREAM_TEST_USER / ACDREAM_TEST_PASS (never logged or
committed), builds a LoginRequest datagram via our codec, sends
it to localhost:9000, waits for up to 5s for a response, and
asserts we receive a ConnectRequest with non-zero cookie, clientId,
and both ISAAC seeds.
Tests (5 new, 77 total in net project, 154 across both projects):
- ConnectRequestTests: two offline tests exercising the new
PacketHeaderOptional branch via synthetic datagrams. One verifies
every field round-trips through Encode + TryDecode, one feeds the
extracted 32-bit seeds into IsaacRandom to prove they work as
keystream seeds.
- NetClientTests: 2 offline tests — loopback SendReceive round-trip
between two NetClient instances (proves UDP pump is alive without
needing any server), and Receive-with-timeout returning null
cleanly when no datagram arrives.
- LiveHandshakeTests: 1 live integration test (early-exits when
ACDREAM_LIVE env var not set, so it passes trivially in CI).
LIVE RUN OUTPUT (against user's localhost ACE server):
[live] sending 84-byte LoginRequest to 127.0.0.1:9000 (user.len=11, pass.len=12)
[live] received 52-byte datagram from 127.0.0.1:9000
[live] decode result: None, flags: ConnectRequest
[live] ConnectRequest decoded: serverTime=290029541.121 cookie=0xAC45998D06754133
clientId=0x00000001 serverSeed=0x4CC09763 clientSeed=0x5C3DE13E
Meaning: 84-byte LoginRequest went out, 52-byte ConnectRequest came
back, codec.TryDecode returned None error, every field parsed to a
sensible value. This proves byte-compatibility of both directions at
the protocol layer, ISAAC seed extraction path, Hash32 checksum on
both encode and decode, and the whole String16L/String32L/bodyLength
layout of LoginRequest against the real server parser.
Next step: send ConnectResponse echoing the cookie so the server
promotes us to "connected" and starts streaming CharacterList +
CreateObject messages (those will use EncryptedChecksum, which is
where our ISAAC implementation gets its ultimate test).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6cda431eae
commit
0cb30aa0c8
5 changed files with 343 additions and 0 deletions
93
tests/AcDream.Core.Net.Tests/Packets/ConnectRequestTests.cs
Normal file
93
tests/AcDream.Core.Net.Tests/Packets/ConnectRequestTests.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Cryptography;
|
||||
using AcDream.Core.Net.Packets;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Packets;
|
||||
|
||||
public class ConnectRequestTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Build a synthetic ConnectRequest packet matching the bytes ACE's
|
||||
/// PacketOutboundConnectRequest would emit, hash it correctly, then
|
||||
/// verify PacketCodec.TryDecode parses the fields back out.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PacketCodec_Decodes_ConnectRequest_Fields()
|
||||
{
|
||||
// Body = 32 bytes of ConnectRequest section.
|
||||
byte[] body = new byte[32];
|
||||
double serverTime = 12345.6789;
|
||||
ulong cookie = 0xFEEDFACECAFEBABEUL;
|
||||
uint clientId = 0x11223344u;
|
||||
uint serverSeed = 0xAABBCCDDu;
|
||||
uint clientSeed = 0x01020304u;
|
||||
|
||||
BinaryPrimitives.WriteInt64LittleEndian(body.AsSpan(0), BitConverter.DoubleToInt64Bits(serverTime));
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(body.AsSpan(8), cookie);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), clientId);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), serverSeed);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(24), clientSeed);
|
||||
// bytes 28-31 = trailing padding uint, zero is fine
|
||||
|
||||
var header = new PacketHeader
|
||||
{
|
||||
Flags = PacketHeaderFlags.ConnectRequest,
|
||||
DataSize = (ushort)body.Length,
|
||||
};
|
||||
|
||||
// Compute unencrypted checksum and pack.
|
||||
uint headerHash = header.CalculateHeaderHash32();
|
||||
uint optionalHash = Hash32.Calculate(body);
|
||||
header.Checksum = headerHash + optionalHash;
|
||||
|
||||
byte[] datagram = new byte[PacketHeader.Size + body.Length];
|
||||
header.Pack(datagram);
|
||||
body.CopyTo(datagram.AsSpan(PacketHeader.Size));
|
||||
|
||||
var result = PacketCodec.TryDecode(datagram, inboundIsaac: null);
|
||||
|
||||
Assert.Equal(PacketCodec.DecodeError.None, result.Error);
|
||||
Assert.Equal(serverTime, result.Packet!.Optional.ConnectRequestServerTime, 1e-6);
|
||||
Assert.Equal(cookie, result.Packet.Optional.ConnectRequestCookie);
|
||||
Assert.Equal(clientId, result.Packet.Optional.ConnectRequestClientId);
|
||||
Assert.Equal(serverSeed, result.Packet.Optional.ConnectRequestServerSeed);
|
||||
Assert.Equal(clientSeed, result.Packet.Optional.ConnectRequestClientSeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectRequest_IsaacSeeds_FeedIsaacRandomSuccessfully()
|
||||
{
|
||||
// Sanity: the 32-bit seeds extracted from a ConnectRequest field are
|
||||
// what IsaacRandom needs to construct an instance. Doesn't prove
|
||||
// the seeds match the server's — that happens at the live test.
|
||||
byte[] body = new byte[32];
|
||||
uint serverSeed = 0xDEADBEEFu;
|
||||
uint clientSeed = 0xCAFEBABEu;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), serverSeed);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(24), clientSeed);
|
||||
|
||||
var header = new PacketHeader { Flags = PacketHeaderFlags.ConnectRequest, DataSize = 32 };
|
||||
header.Checksum = header.CalculateHeaderHash32() + Hash32.Calculate(body);
|
||||
|
||||
byte[] datagram = new byte[PacketHeader.Size + 32];
|
||||
header.Pack(datagram);
|
||||
body.CopyTo(datagram.AsSpan(PacketHeader.Size));
|
||||
|
||||
var result = PacketCodec.TryDecode(datagram, inboundIsaac: null);
|
||||
Assert.Equal(PacketCodec.DecodeError.None, result.Error);
|
||||
|
||||
// Reconstruct the seed bytes and feed IsaacRandom.
|
||||
var serverSeedBytes = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(serverSeedBytes, result.Packet!.Optional.ConnectRequestServerSeed);
|
||||
var clientSeedBytes = new byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(clientSeedBytes, result.Packet.Optional.ConnectRequestClientSeed);
|
||||
|
||||
var serverIsaac = new IsaacRandom(serverSeedBytes);
|
||||
var clientIsaac = new IsaacRandom(clientSeedBytes);
|
||||
|
||||
// Just verify they produce nonzero output (not a correctness check).
|
||||
uint firstServerKey = serverIsaac.Next();
|
||||
uint firstClientKey = clientIsaac.Next();
|
||||
Assert.NotEqual(firstServerKey, firstClientKey);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue