Implement 40250 classic client port
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
#include "classic_stream.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace mtnet::classic {
|
||||
|
||||
namespace {
|
||||
constexpr size_t RECV_CHUNK = 32 * 1024;
|
||||
|
||||
uint32_t now_ms() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint32_t>(
|
||||
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
void set_nonblocking(int fd) {
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ClassicStream::~ClassicStream() {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void ClassicStream::set_state(State s) {
|
||||
if (m_state == s) {
|
||||
return;
|
||||
}
|
||||
m_state = s;
|
||||
if (on_state_change) {
|
||||
on_state_change(s);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------- socket
|
||||
bool ClassicStream::connect(const std::string &host, uint16_t port) {
|
||||
disconnect();
|
||||
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo *res = nullptr;
|
||||
char portbuf[8];
|
||||
std::snprintf(portbuf, sizeof(portbuf), "%u", port);
|
||||
if (getaddrinfo(host.c_str(), portbuf, &hints, &res) != 0 || !res) {
|
||||
m_last_error = "getaddrinfo(" + host + ") failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_sock = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
if (m_sock < 0) {
|
||||
m_last_error = std::string("socket: ") + std::strerror(errno);
|
||||
freeaddrinfo(res);
|
||||
return false;
|
||||
}
|
||||
set_nonblocking(m_sock);
|
||||
int one = 1;
|
||||
setsockopt(m_sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
||||
|
||||
int rc = ::connect(m_sock, res->ai_addr, res->ai_addrlen);
|
||||
freeaddrinfo(res);
|
||||
if (rc == 0) {
|
||||
set_state(State::Online);
|
||||
} else if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
|
||||
set_state(State::Connecting);
|
||||
} else {
|
||||
m_last_error = std::string("connect: ") + std::strerror(errno);
|
||||
::close(m_sock);
|
||||
m_sock = -1;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClassicStream::disconnect() {
|
||||
if (m_sock >= 0) {
|
||||
::close(m_sock);
|
||||
m_sock = -1;
|
||||
}
|
||||
m_recv.clear();
|
||||
m_send.clear();
|
||||
m_seq_idx = 0;
|
||||
m_seq_on = false;
|
||||
m_handshake_seen = false;
|
||||
m_cipher.clean_up();
|
||||
set_state(State::Offline);
|
||||
}
|
||||
|
||||
void ClassicStream::decrypt_appended(size_t n) {
|
||||
if (n == 0 || !m_cipher.activated()) {
|
||||
return;
|
||||
}
|
||||
// the n bytes just written sit at the tail of the unread region
|
||||
m_cipher.decrypt(m_recv.mutable_unread() + (m_recv.readable() - n), n);
|
||||
}
|
||||
|
||||
bool ClassicStream::recv_into_buffer() {
|
||||
uint8_t *dst = m_recv.reserve_write(RECV_CHUNK);
|
||||
ssize_t n = ::recv(m_sock, dst, RECV_CHUNK, 0);
|
||||
if (n > 0) {
|
||||
m_recv.commit_write(static_cast<size_t>(n), RECV_CHUNK);
|
||||
decrypt_appended(static_cast<size_t>(n));
|
||||
return true;
|
||||
}
|
||||
m_recv.commit_write(0, RECV_CHUNK);
|
||||
if (n == 0) {
|
||||
m_last_error = "peer closed";
|
||||
return false;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return true;
|
||||
}
|
||||
m_last_error = std::string("recv: ") + std::strerror(errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClassicStream::flush_send() {
|
||||
while (m_send.readable() > 0) {
|
||||
ssize_t n = ::send(m_sock, m_send.read_ptr(), m_send.readable(), 0);
|
||||
if (n > 0) {
|
||||
m_send.discard(static_cast<size_t>(n));
|
||||
continue;
|
||||
}
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
break;
|
||||
}
|
||||
m_last_error = std::string("send: ") + std::strerror(errno);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClassicStream::process() {
|
||||
if (m_sock < 0) {
|
||||
return;
|
||||
}
|
||||
if (m_state == State::Connecting) {
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
if (getsockopt(m_sock, SOL_SOCKET, SO_ERROR, &err, &len) < 0 || err != 0) {
|
||||
m_last_error = "connect failed";
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
set_state(State::Online);
|
||||
}
|
||||
if (!recv_into_buffer()) {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
dispatch();
|
||||
if (m_sock >= 0 && !flush_send()) {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- framing core
|
||||
void ClassicStream::feed(const void *data, size_t n) {
|
||||
m_recv.write(data, n);
|
||||
decrypt_appended(n);
|
||||
dispatch();
|
||||
}
|
||||
|
||||
void ClassicStream::emit_bytes(const void *p, size_t n) {
|
||||
if (!m_cipher.activated()) {
|
||||
m_send.write(p, n);
|
||||
return;
|
||||
}
|
||||
// CTR stream: encrypt in call order; the encoder keeps its counter across calls.
|
||||
std::string tmp(static_cast<const char *>(p), n);
|
||||
m_cipher.encrypt(tmp.data(), n);
|
||||
m_send.write(tmp.data(), n);
|
||||
}
|
||||
|
||||
size_t ClassicStream::take_outgoing(void *dst, size_t cap) {
|
||||
size_t n = m_send.readable() < cap ? m_send.readable() : cap;
|
||||
if (n) {
|
||||
std::memcpy(dst, m_send.read_ptr(), n);
|
||||
m_send.discard(n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void ClassicStream::append_sequence_if_needed(uint8_t header) {
|
||||
if (!m_seq_on || !is_sequence_cg(header)) {
|
||||
return;
|
||||
}
|
||||
uint8_t seq = SEQUENCE_TABLE[m_seq_idx];
|
||||
m_seq_idx = (m_seq_idx + 1) % SEQUENCE_TABLE_SIZE;
|
||||
emit_bytes(&seq, 1);
|
||||
}
|
||||
|
||||
bool ClassicStream::send_fixed(const void *struct_bytes, size_t n) {
|
||||
if (n == 0) {
|
||||
return false;
|
||||
}
|
||||
uint8_t header = *static_cast<const uint8_t *>(struct_bytes);
|
||||
emit_bytes(struct_bytes, n);
|
||||
append_sequence_if_needed(header);
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] send hdr=%u n=%zu seq=%d\n", header, n,
|
||||
(m_seq_on && is_sequence_cg(header)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClassicStream::send_dynamic(const void *bytes, size_t n) {
|
||||
if (n < sizeof(DynHead)) {
|
||||
return false;
|
||||
}
|
||||
uint8_t header = *static_cast<const uint8_t *>(bytes);
|
||||
emit_bytes(bytes, n);
|
||||
append_sequence_if_needed(header);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- dispatch
|
||||
bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
||||
consumed = false;
|
||||
switch (header) {
|
||||
case HDR_HANDSHAKE: {
|
||||
Handshake hs{};
|
||||
if (!m_recv.peek(&hs, sizeof(hs))) {
|
||||
return true; // need more
|
||||
}
|
||||
m_recv.discard(sizeof(hs));
|
||||
consumed = true;
|
||||
// mirror EterLib/PythonNetworkStreamPhaseHandshake.cpp + desc.cpp:
|
||||
m_server_time_base = hs.time + static_cast<uint32_t>(hs.delta);
|
||||
m_client_time_base = now_ms();
|
||||
hs.time = hs.time + 2u * static_cast<uint32_t>(hs.delta);
|
||||
hs.delta = 0;
|
||||
const bool initial = !m_handshake_seen;
|
||||
m_handshake_seen = true;
|
||||
if (!initial && m_time_sync_mode) {
|
||||
// PythonNetworkStreamPhaseHandShake.cpp uses 0xFF only for the
|
||||
// first handshake. Select/Game phase resyncs are sent as the
|
||||
// same struct with header 0xFC and the next sequence byte.
|
||||
hs.header = HDR_CG_TIME_SYNC;
|
||||
send_fixed(&hs, sizeof(hs));
|
||||
} else {
|
||||
// Initial handshake (and the mark side's repeated login
|
||||
// handshake) stays 0xFF and has no sequence byte.
|
||||
emit_bytes(&hs, sizeof(hs));
|
||||
}
|
||||
if (on_server_handshake) {
|
||||
on_server_handshake();
|
||||
}
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] handshake echo time=%u\n", hs.time);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case HDR_GC_TIME_SYNC: { // 0xFC — 1-byte "handshake ok" blank (re-sync path)
|
||||
Blank b{};
|
||||
if (!m_recv.peek(&b, sizeof(b))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(b));
|
||||
consumed = true;
|
||||
if (on_handshake_ok) {
|
||||
on_handshake_ok();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case HDR_GC_PHASE: {
|
||||
Phase_ p{};
|
||||
if (!m_recv.peek(&p, sizeof(p))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(p));
|
||||
consumed = true;
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] phase -> %u\n", p.phase);
|
||||
}
|
||||
if (on_phase) {
|
||||
on_phase(p.phase);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case HDR_GC_PING: {
|
||||
Blank b{};
|
||||
if (!m_recv.peek(&b, sizeof(b))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(b));
|
||||
consumed = true;
|
||||
uint8_t pong = HDR_CG_PONG; // 0xFE
|
||||
send_fixed(&pong, 1); // CG_PONG is bSeq=true (sizeof(BYTE)+seq)
|
||||
return true;
|
||||
}
|
||||
case HDR_GC_BINDUDP: {
|
||||
BindUDP u{};
|
||||
if (!m_recv.peek(&u, sizeof(u))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(u));
|
||||
consumed = true;
|
||||
return true; // UDP not used
|
||||
}
|
||||
case HDR_KEY_AGREEMENT: { // 0xFB — GC_KEY_AGREEMENT (server's DH2 blob)
|
||||
if (m_recv.readable() < sizeof(KeyAgreement)) {
|
||||
return true; // need the whole 261-byte packet (consumed stays false)
|
||||
}
|
||||
consumed = true;
|
||||
return handle_key_agreement();
|
||||
}
|
||||
case HDR_GC_KEY_AGREEMENT_COMPLETED: { // 0xFA
|
||||
KeyAgreementCompleted kac{};
|
||||
if (!m_recv.peek(&kac, sizeof(kac))) {
|
||||
return true;
|
||||
}
|
||||
m_recv.discard(sizeof(kac));
|
||||
consumed = true;
|
||||
m_cipher.set_activated(true);
|
||||
// the server may have pipelined encrypted bytes right behind this;
|
||||
// decrypt whatever's already buffered before we dispatch it.
|
||||
if (m_recv.readable() > 0) {
|
||||
m_cipher.decrypt(m_recv.mutable_unread(), m_recv.readable());
|
||||
}
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] cipher activated\n");
|
||||
}
|
||||
if (on_cipher_active) {
|
||||
on_cipher_active();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return true; // not a control header
|
||||
}
|
||||
}
|
||||
|
||||
bool ClassicStream::handle_key_agreement() {
|
||||
KeyAgreement pkt{};
|
||||
if (!m_recv.peek(&pkt, sizeof(pkt))) {
|
||||
return true; // need the whole 261-byte packet
|
||||
}
|
||||
m_recv.discard(sizeof(pkt));
|
||||
|
||||
// generate our own DH2 public blob
|
||||
KeyAgreement out{};
|
||||
out.header = HDR_KEY_AGREEMENT; // CG_KEY_AGREEMENT (same 0xFB)
|
||||
size_t data_len = sizeof(out.data);
|
||||
size_t agreed = m_cipher.prepare(out.data, &data_len);
|
||||
if (agreed == 0) {
|
||||
m_last_error = "classic cipher: DH2 Prepare failed";
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
out.agreed_length = static_cast<uint16_t>(agreed);
|
||||
out.data_length = static_cast<uint16_t>(data_len);
|
||||
|
||||
// derive keys from the peer's blob (does NOT flip activated_ yet)
|
||||
if (!m_cipher.activate(m_polarity, pkt.agreed_length, pkt.data, pkt.data_length)) {
|
||||
m_last_error = "classic cipher: DH2 Agree/SetUp failed";
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// reply with our blob — still plaintext (peer activates on its own COMPLETED)
|
||||
emit_bytes(&out, sizeof(out));
|
||||
if (m_trace) {
|
||||
std::fprintf(stderr, "[classic] key agreement: replied, keys ready\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClassicStream::dispatch() {
|
||||
for (;;) {
|
||||
if (m_recv.readable() == 0) {
|
||||
return;
|
||||
}
|
||||
uint8_t header = *m_recv.read_ptr();
|
||||
|
||||
// zero padding between packets (cipher block alignment on the m2dev side;
|
||||
// harmless to skip here too).
|
||||
if (header == 0) {
|
||||
m_recv.discard(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool consumed = false;
|
||||
if (!handle_control(header, consumed)) {
|
||||
if (on_error && m_last_error.empty()) {
|
||||
on_error("dispatch aborted");
|
||||
}
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (consumed) {
|
||||
continue;
|
||||
}
|
||||
// control handler said "need more bytes" for a control header it owns?
|
||||
if (header == HDR_HANDSHAKE || header == HDR_GC_TIME_SYNC || header == HDR_GC_PHASE ||
|
||||
header == HDR_GC_PING || header == HDR_GC_BINDUDP || header == HDR_KEY_AGREEMENT ||
|
||||
header == HDR_GC_KEY_AGREEMENT_COMPLETED) {
|
||||
return; // wait for the rest of this control packet
|
||||
}
|
||||
|
||||
// Some side connections share the classic cipher/control handshake but
|
||||
// have their own packet framing. Give that connection a chance before
|
||||
// the normal GC static/dynamic table is consulted.
|
||||
if (on_raw_packet) {
|
||||
RawPacketResult raw = on_raw_packet(header, m_recv.read_ptr(), m_recv.readable());
|
||||
switch (raw.status) {
|
||||
case RawPacketStatus::NeedMore:
|
||||
return;
|
||||
case RawPacketStatus::Consumed:
|
||||
if (raw.bytes == 0 || raw.bytes > m_recv.readable()) {
|
||||
m_last_error = "raw packet consumed invalid byte count";
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
m_recv.discard(raw.bytes);
|
||||
continue;
|
||||
case RawPacketStatus::Error:
|
||||
m_last_error = "raw packet framing error";
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
disconnect();
|
||||
return;
|
||||
case RawPacketStatus::NotHandled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- dynamic-size GC packet ---
|
||||
if (is_dynamic_gc(header)) {
|
||||
DynHead dh{};
|
||||
if (!m_recv.peek(&dh, sizeof(dh))) {
|
||||
return;
|
||||
}
|
||||
if (dh.size < sizeof(DynHead)) {
|
||||
m_last_error = "dynamic packet size underflow";
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (m_recv.readable() < dh.size) {
|
||||
return; // whole packet not here yet
|
||||
}
|
||||
m_recv.discard(sizeof(DynHead));
|
||||
uint32_t body_len = dh.size - sizeof(DynHead);
|
||||
const uint8_t *body = m_recv.read_ptr();
|
||||
bool ok = !on_packet || on_packet(header, body, body_len);
|
||||
m_recv.discard(body_len);
|
||||
if (!ok) {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- static-size GC packet --- (packet_size_gc = FULL size incl. header)
|
||||
int total = packet_size_gc(header);
|
||||
if (total <= 0) {
|
||||
m_last_error = "unknown/unsupported GC header " + std::to_string(header);
|
||||
if (on_error) {
|
||||
on_error(m_last_error);
|
||||
}
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
if (m_recv.readable() < static_cast<size_t>(total)) {
|
||||
return;
|
||||
}
|
||||
const uint8_t *p = m_recv.read_ptr();
|
||||
bool ok = !on_packet || on_packet(header, p + 1, static_cast<uint32_t>(total - 1));
|
||||
m_recv.discard(static_cast<size_t>(total));
|
||||
if (!ok) {
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mtnet::classic
|
||||
Reference in New Issue
Block a user