diff --git a/.gitignore b/.gitignore index a320255..8311cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,5 +53,8 @@ Data/ Logs/ **/Logs/ +### Diagnostic probe state - a runtime cursor, not configuration (see AnsVerifyProbe) ### +Config/AuthServer/AnsVerifyProbe.state + ### IntelliJ build artifacts (jars are built from source; no need to version them) ### out/ diff --git a/Config/AuthServer/AuthServer.ini b/Config/AuthServer/AuthServer.ini index 5c69af2..82195fa 100644 --- a/Config/AuthServer/AuthServer.ini +++ b/Config/AuthServer/AuthServer.ini @@ -2,14 +2,22 @@ # Found inside CLIENT_INFO.INI in the System.wpk on the SUN client. CLIENT_VERSION = 2.6.0.1 # Typically found in the bottom left corner of the login/server-select screen on the SUN client. This NEEDS to be checked! -CLIENT_PROTOCOL = 3.4.6 +# Original: 3.4.6 +CLIENT_PROTOCOL = 7.1.1 # Launcher's version. Found at Config/Launcher/Launcher.ini. LAUNCHER_VERSION = 1.0.0 [NETWORK] -# Determines whether one connection per IP address is allowed. +# Determines whether one connection per IP address is allowed on the client-facing listener. +# Was read by AuthServerConfig but never applied by NioServer until now; the filter used to be +# added unconditionally, so this line had no effect. +# KEEP THIS FALSE FOR LOCAL CLIENT TESTING. The launcher and the game client are both 127.0.0.1, +# and the launcher deliberately holds its AuthServer connection open across the handoff +# (LauncherController#onStartGame). With the filter on, the launcher owns the address and the +# game client is disconnected before it can send U2A_askVerify - see CLIENT-PROTOCOL-NOTES.md +# section 10. # DEFAULT: TRUE OPTIONS: TRUE | FALSE -UNIQUE_IP_FILTER = TRUE +UNIQUE_IP_FILTER = FALSE # This will force the connection to drop when read or write thread(s) have been idle for x-amount of time. Setting this to 0 will disable the disconnect feature. # DEFAULT: 300 (5 MINUTES) VALUE: SECONDS DISCONNECT = 300 diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsSrvList.java b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsSrvList.java index 7bf1acf..c47afe6 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsSrvList.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsSrvList.java @@ -1,29 +1,175 @@ package com.valiantgaming.authserver.network.packet.client; +import com.valiantgaming.authserver.database.entity.server.ServerInfo; +import com.valiantgaming.authserver.network.session.server.GameServerRegistry; import com.valiantgaming.commons.network.packet.Category; import com.valiantgaming.commons.network.packet.Protocol; +import lombok.extern.log4j.Log4j2; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; /** - * Builds {@code A2U_ansSrvList_Srv} (server list) and {@code A2U_ansSrvList_Chn} (channel - * list for a selected server), sent in response to {@code U2A_askSrvList}. + * Builds {@code A2U_ansSrvList_Srv} (the server list) and {@code A2U_ansSrvList_Chn} (the channel + * list), sent in response to {@code U2A_askSrvList}. + * + *

Layouts come from the EP1 {@code ServerPackets} documentation, transcribed in + * {@code CLIENT-PROTOCOL-NOTES.md} §11.4. The 2-byte little-endian length header is added by + * {@code ClientPacketEncoder}, so everything below starts at the category byte: + * + *

+ * Servers  0x33 | 0x11 | count(1) | entry [ 0x00 entry ]*
+ *          entry = Name(32, null-padded) | Unknown(1) | Server#(1) | Unknown(1)
+ *
+ * Channels 0x33 | 0x12 | count(1) | entry [ 0x00 entry ]*
+ *          entry = Name(33, null-padded) | Server#(1) | Channel#(1) | Terminator(1, NOT 0x00)
+ * 
+ * + *

Note the asymmetry, which is easy to get wrong: server names are 32 bytes, channel + * names are 33. The single {@code 0x00} separator sits between entries only - there + * is none after the last one. * - *

Neither packet's payload layout has been captured, and there's no S2S query yet for - * auth-server to ask database-server for the live {@code ServerInfo} rows it would need to - * populate a real list (see {@code DatabaseManager}, which already loads them at startup on - * the database-server side). Both methods below are placeholders that report zero entries - - * revisit once both the wire format and that S2S query exist. + *

These layouts are EP1-era and unverified against the Classic client. §11 found the + * request side unchanged between generations and {@code U2A_askSrvList} confirmed exactly + * ({@code 33 0F}, no body - §13.3), but {@code askAuthUser} did change, so a response layout + * surviving is not guaranteed. The three unknown/terminator bytes are the likeliest thing to be + * wrong - see {@link #SERVER_NAME_TERMINATOR} and {@link #SERVER_ENTRY_TERMINATOR}. + * + *

Data source, and it is the wrong one. Servers come from {@link GameServerRegistry}, + * which holds the one {@code ServerInfo} row the S2S handshake already fetches - but + * {@code ServerInfo} is the infrastructure registry (AUTH/GAME/DB/WEB and their S2S ports), not + * player-facing worlds. Channels are worse: nothing queries them, so one is synthesised per server + * (see {@link #CHANNEL_NAME}). Both were enough to prove the layouts against a real client (§16) + * and neither is right. + * + *

The real sources are the {@code GameServerInfo} and {@code ChannelInfo} tables, which exist + * and are empty, and which need stored procedures, DAOs and an S2S list query before this can read + * them - see {@code CLIENT-PROTOCOL-NOTES.md} §18 for the full handoff. */ +@Log4j2 public class AnsSrvList { + /** Fixed width of a server name, null-padded. Channel names are one byte wider - see below. */ + private static final int SERVER_NAME_LENGTH = 32; + + /** Fixed width of a channel name, null-padded. Deliberately 33, not 32 - see the class comment. */ + private static final int CHANNEL_NAME_LENGTH = 33; + + /** Single {@code 0x00} written between entries, never after the last. */ + private static final byte ENTRY_SEPARATOR = 0x00; + + /** + * The documentation's first "Unknown (1 byte)" in a server entry, sitting between the name and + * the server number. Written as {@code 0x00}, which is what the documentation implies and also + * the reading that makes the server name field effectively 33 bytes like the channel one. + */ + private static final byte SERVER_NAME_TERMINATOR = 0x00; + + /** + * The documentation's second "Unknown (1 byte)", closing a server entry. + * + *

Prime suspect if the client rejects the server list. The documentation gives no + * value for it, but the channel entry has a byte in the same position that it explicitly says + * "cannot be 0x00". If these two fields are the same thing, this should be + * {@link #CHANNEL_TERMINATOR} instead. Left at the literal documented reading until a client + * says otherwise; flipping it is a one-line change. + */ + private static final byte SERVER_ENTRY_TERMINATOR = 0x00; + + /** Closes a channel entry. The documentation is explicit that this must not be {@code 0x00}. */ + private static final byte CHANNEL_TERMINATOR = 0x01; + + /** + * Placeholder channel name. Nothing in this stack models channels - there is no table, no + * config and no S2S query for them - so one channel is synthesised per server purely so the + * client has something selectable and can be driven on to {@code U2A_askSrvSelect}. + */ + private static final String CHANNEL_NAME = "Channel 1"; + + /** + * Numbering handed to the client for both servers and channels. Zero-based on the assumption + * that these are indexes rather than display numbers - unconfirmed. The client echoes + * both back in {@code U2A_askSrvSelect} (see {@code SrvSelect}, which logs the payload), so the + * first selection a client makes settles the convention. + */ + private static final int FIRST_NUMBER = 0; + public byte[] createServerListPacket() { - // TODO: replace with real ServerInfo entries once an S2S "list servers" query exists. - return new byte[] { Category.AUTH, Protocol.A2U_ansSrvList_Srv, 0x00 }; + List servers = GameServerRegistry.getServers(); + + if(servers.isEmpty()) + log.warn("Answering askSrvList with an empty server list - no ServerInfo from database-server yet. " + + "Start database-server before auth-server; the S2S connection is never retried."); + + ByteArrayOutputStream body = new ByteArrayOutputStream(); + body.write(servers.size()); + + for(int index = 0; index < servers.size(); index++) + { + if(index > 0) + body.write(ENTRY_SEPARATOR); + + writeFixed(body, servers.get(index).getServerName(), SERVER_NAME_LENGTH); + body.write(SERVER_NAME_TERMINATOR); + body.write(FIRST_NUMBER + index); + body.write(SERVER_ENTRY_TERMINATOR); + } + + return packet(Protocol.A2U_ansSrvList_Srv, body.toByteArray()); } + /** + * One synthesised channel per server, so a client that picks a server has something to pick + * inside it. See {@link #CHANNEL_NAME} for why this is not real data. + */ public byte[] createChannelListPacket() { - // TODO: replace with real channel/sub-server entries once the wire format is known. - return new byte[] { Category.AUTH, Protocol.A2U_ansSrvList_Chn, 0x00 }; + List servers = GameServerRegistry.getServers(); + + ByteArrayOutputStream body = new ByteArrayOutputStream(); + body.write(servers.size()); + + for(int index = 0; index < servers.size(); index++) + { + if(index > 0) + body.write(ENTRY_SEPARATOR); + + writeFixed(body, CHANNEL_NAME, CHANNEL_NAME_LENGTH); + body.write(FIRST_NUMBER + index); + body.write(FIRST_NUMBER); + body.write(CHANNEL_TERMINATOR); + } + + return packet(Protocol.A2U_ansSrvList_Chn, body.toByteArray()); + } + + /** + * Writes {@code value} as exactly {@code length} bytes, null-padded, truncating anything + * longer. Truncation is logged rather than silent: a name clipped mid-way is the sort of thing + * that shows up as an unreadable entry on the client's screen and is otherwise hard to trace. + */ + private static void writeFixed(ByteArrayOutputStream out, String value, int length) + { + byte[] bytes = (value == null ? "" : value).getBytes(StandardCharsets.US_ASCII); + + if(bytes.length > length) + log.warn("Name '{}' is {} bytes and does not fit the {}-byte field - truncating.", value, bytes.length, length); + + out.write(bytes, 0, Math.min(bytes.length, length)); + + for(int i = bytes.length; i < length; i++) + out.write(0x00); + } + + private static byte[] packet(byte protocol, byte[] body) + { + byte[] packet = new byte[2 + body.length]; + packet[0] = Category.AUTH; + packet[1] = protocol; + System.arraycopy(body, 0, packet, 2, body.length); + + return packet; } } diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/handler/VerifyUser.java b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/handler/VerifyUser.java index 4d26450..cd4a6e2 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/handler/VerifyUser.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/handler/VerifyUser.java @@ -1,5 +1,6 @@ package com.valiantgaming.authserver.network.packet.client.handler; +import com.valiantgaming.authserver.config.AuthServerConfig; import com.valiantgaming.commons.utility.Utility; import lombok.extern.log4j.Log4j2; diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsLauncherReady.java b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsLauncherReady.java similarity index 92% rename from auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsLauncherReady.java rename to auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsLauncherReady.java index 85a092c..877f8bb 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsLauncherReady.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsLauncherReady.java @@ -1,4 +1,4 @@ -package com.valiantgaming.authserver.network.packet.client; +package com.valiantgaming.authserver.network.packet.client.launcher; import com.valiantgaming.commons.network.packet.Category; import com.valiantgaming.commons.network.packet.Protocol; diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsVerifyVersion.java b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsVerifyVersion.java similarity index 96% rename from auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsVerifyVersion.java rename to auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsVerifyVersion.java index d2af6ae..bab8a4a 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/AnsVerifyVersion.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/network/packet/client/launcher/AnsVerifyVersion.java @@ -1,4 +1,4 @@ -package com.valiantgaming.authserver.network.packet.client; +package com.valiantgaming.authserver.network.packet.client.launcher; import com.valiantgaming.authserver.config.AuthServerConfig; import com.valiantgaming.commons.network.packet.Category; diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/network/session/server/GameServerRegistry.java b/auth-server/src/main/java/com/valiantgaming/authserver/network/session/server/GameServerRegistry.java new file mode 100644 index 0000000..046c1e6 --- /dev/null +++ b/auth-server/src/main/java/com/valiantgaming/authserver/network/session/server/GameServerRegistry.java @@ -0,0 +1,60 @@ +package com.valiantgaming.authserver.network.session.server; + +import com.valiantgaming.authserver.database.entity.server.ServerInfo; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +/** + * The game servers auth-server can offer a client in {@code A2U_ansSrvList_Srv}. + * + *

Today that is exactly one row, and it arrives for free: the S2S handshake already asks + * database-server for {@code "GAME SERVER"} ({@code AskServerInfo} -> {@code PacketHandler}'s + * {@code S2S_askServerInfo} branch) and gets back a full {@code ServerInfo} with its address and + * port. {@code ServerPacketHandler} used to decode that, log it and drop it on the floor; it is + * kept here instead so {@code AnsSrvList} has real data to answer with rather than a placeholder. + * + *

This is a stopgap on the wrong table. {@code ServerInfo} is this stack's own + * infrastructure registry - its four rows are DATABASE/AUTH/GAME/WEB SERVER and their S2S + * addresses, not player-facing worlds. It was used because it is the only live data auth-server + * has, and it was enough to prove the {@code ansSrvList} layouts against a real client (§16). + * + *

The right sources are the {@code GameServerInfo} and {@code ChannelInfo} tables, which already + * exist and are empty - see {@code CLIENT-PROTOCOL-NOTES.md} §18. The distinction bites at + * {@code A2U_ansSrvSelect}: the port in this row is the game server's S2S listener, and handing it + * to a client would point it at the wrong socket. + * + *

Empty until the S2S handshake completes, which is normal at startup and for as long as + * database-server is unreachable - {@code NioServer#initS2S} connects once and never retries, so + * an auth-server started first stays empty until it is restarted. + */ +public final class GameServerRegistry +{ + private static final AtomicReference gameServer = new AtomicReference<>(); + + private GameServerRegistry() + { + } + + public static void setGameServer(ServerInfo serverInfo) + { + gameServer.set(serverInfo); + } + + public static Optional getGameServer() + { + return Optional.ofNullable(gameServer.get()); + } + + /** + * Every server that can be offered to a client, in the order the client will index them. + * Empty rather than null when nothing is known yet, so callers can build an empty list packet. + */ + public static List getServers() + { + ServerInfo serverInfo = gameServer.get(); + + return serverInfo == null ? List.of() : List.of(serverInfo); + } +} diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/server/NioServer.java b/auth-server/src/main/java/com/valiantgaming/authserver/server/NioServer.java index 7dbb9d4..1ac2d09 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/server/NioServer.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/server/NioServer.java @@ -154,8 +154,20 @@ private void initC2S() @Override protected void initChannel(Channel ch) { - // UniqueIpFilter only allows one IP per channel, so a client cannot connect more than once. - ch.pipeline().addLast("uniqueIpFilter", new UniqueIpFilter()); + // UniqueIpFilter only allows one connection per remote IP - a second one is closed + // the moment it is accepted. + // + // This is now gated on [NETWORK] UNIQUE_IP_FILTER, which the ini has always carried + // and AuthServerConfig has always read, but which was never consulted here: the + // filter was added unconditionally, so setting the flag FALSE did nothing. + // + // It has to be switchable because on a local dev box the launcher and the game + // client are both 127.0.0.1, and the launcher deliberately holds its AuthServer + // connection open across the handoff (LauncherController#onStartGame). With the + // filter on, that connection owns the address and the game client's connection is + // closed before it can send U2A_askVerify - see CLIENT-PROTOCOL-NOTES.md section 10. + if(AuthServerConfig.isUniqueIpFilter()) + ch.pipeline().addLast("uniqueIpFilter", new UniqueIpFilter()); // Inactivity Handler. Ping the client every # seconds of inactivity and DC client after # seconds. ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, disconnect)); diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ClientPacketHandler.java b/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ClientPacketHandler.java index aa2b605..052c13e 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ClientPacketHandler.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ClientPacketHandler.java @@ -5,6 +5,8 @@ import com.valiantgaming.authserver.network.packet.client.handler.AuthUser; import com.valiantgaming.authserver.network.packet.client.handler.SrvSelect; import com.valiantgaming.authserver.network.packet.client.handler.VerifyUser; +import com.valiantgaming.authserver.network.packet.client.launcher.AnsLauncherReady; +import com.valiantgaming.authserver.network.packet.client.launcher.AnsVerifyVersion; import com.valiantgaming.authserver.network.packet.server.AskAuthUser; import com.valiantgaming.authserver.network.session.client.ClientSession; import com.valiantgaming.authserver.network.session.client.ClientSessionManager; @@ -19,6 +21,7 @@ import lombok.extern.log4j.Log4j2; import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; /** * Dispatches both flows documented in {@code Protocol} that share this listener: the game @@ -42,14 +45,50 @@ *

Several of the game-client packets' real payload layouts are unconfirmed and their * answers are placeholders - see the class comments on {@code AnsVerify}/{@code AnsSrvList}/ * {@code AnsSrvSelect} and on {@code AuthUser} for exactly what's still stubbed. + * + *

Reading the log

+ * Every line carries a {@code [conn-N]} tag, and connections that never send a byte are logged + * at DEBUG rather than INFO. Both exist because this listener is not only spoken to by the game + * client: the launcher opens a persistent connection at startup ({@code NioClient}) and + * re-connects every five seconds as a reachability probe + * ({@code ServerHealthCheck#isReachable}), so at INFO the log used to be a stream of + * connect/disconnect pairs no different in shape from a real client's. + * + *

A heartbeat probe is now a DEBUG connect and a DEBUG close with {@code 0 packet(s)}; a real + * game client is the only thing that produces INFO {@code [conn-N] <- } lines. So "did the client + * answer our {@code A2U_ansVerify}?" is answered by whether {@code conn-N} logs a second inbound + * packet, and the close line for that same {@code conn-N} states the total either way. */ @Log4j2 public class ClientPacketHandler extends ChannelDuplexHandler { + /** Per-JVM connection counter, so every log line can name which connection it belongs to. */ + private static final AtomicLong CONNECTION_COUNTER = new AtomicLong(); + + /** + * How many times one connection may be sent {@code AnsVerifyProbe}'s re-trigger. Diagnostic + * only, and only a runaway guard: the client re-verifies on every re-trigger, so without a + * bound the two would ping-pong forever, spin the log and wrap the candidate rotation. Sized to + * comfortably clear the 32-entry opcode sweep in one launch. + */ + private static final int MAX_PROBE_RETRIGGERS = 40; + + // One handler instance per channel (NioServer#initC2S news one up in initChannel), so these + // are per-connection state, not shared. + private final long connectionId = CONNECTION_COUNTER.incrementAndGet(); + private long connectedAt; + private int packetsReceived; + private int probeRetriggersSent; + @Override public void channelActive(ChannelHandlerContext ctx) { - log.info("Client connected from " + ctx.channel().remoteAddress()); + connectedAt = System.nanoTime(); + + // DEBUG, not INFO: most connections here are the launcher's five-second reachability + // probe, which connects and closes without speaking. channelRead announces anything that + // actually says something. + log.debug("[conn-{}] Client connected from {}", connectionId, ctx.channel().remoteAddress()); ClientSessionManager.getInstance().addSession(ctx); ClientSession session = ClientSessionManager.getInstance().getSession(ctx); @@ -61,7 +100,12 @@ public void channelActive(ChannelHandlerContext ctx) public void channelRead(ChannelHandlerContext ctx, Object msg) { byte[] message = (byte[]) msg; - log.info("Message: {}", Utility.byteArrayToHexString(message)); + + if(++packetsReceived == 1) + log.info("[conn-{}] First inbound packet from {} - this connection is a real client, not a health-check probe.", + connectionId, ctx.channel().remoteAddress()); + + log.info("[conn-{}] <- #{} {}", connectionId, packetsReceived, Utility.byteArrayToHexString(message)); ClientSession session = ClientSessionManager.getInstance().getSession(ctx); @@ -71,7 +115,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { case Protocol.L2A_askUnknown1: { - log.info("Launcher handshake started by {}", ctx.channel().remoteAddress()); + log.info("[conn-{}] Launcher handshake started by {}", connectionId, ctx.channel().remoteAddress()); ctx.writeAndFlush(new AnsLauncherReady().createPacket()); ctx.writeAndFlush(new AnsVerifyVersion().createPacket()); @@ -80,7 +124,14 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) case Protocol.U2A_askVerify: { byte[] payload = VerifyUser.decode(message); - log.info("Received verify request, payload: {}", Utility.byteArrayToHexString(payload)); + log.info("[conn-{}] Received verify request, payload: {}", connectionId, Utility.byteArrayToHexString(payload)); + + if(AuthServerConfig.isAnsVerifyProbe()) + { + // Diagnostic mode: serve a different candidate per connection to find the + // real response format. See AnsVerifyProbe. + AnsVerifyProbe.Attempt attempt = AnsVerifyProbe.next(payload); + log.info("[conn-{}] PROBE serving ansVerify candidate {} -> {}", connectionId, attempt.label(), attempt.hex()); if(AuthServerConfig.isAnsVerifyProbe()) { @@ -107,7 +158,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) if(dbChannel == null || !dbChannel.isActive()) { - log.error("No active connection to Database Server - cannot authenticate {}", credentials.username()); + log.error("[conn-{}] No active connection to Database Server - cannot authenticate {}", connectionId, credentials.username()); ctx.writeAndFlush(AnsAuthUser.createPacket(false)); break; } @@ -120,11 +171,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) } case Protocol.U2A_askSrvList: { - if(!session.isAuthenticated()) - { - log.warn("Client {} asked for the server list before authenticating - dropping.", ctx.channel().remoteAddress()); + if(!isAllowed(session, ctx, "the server list")) return; - } AnsSrvList ansSrvList = new AnsSrvList(); ctx.writeAndFlush(ansSrvList.createServerListPacket()); @@ -133,21 +181,18 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) } case Protocol.U2A_askSrvSelect: { - if(!session.isAuthenticated()) - { - log.warn("Client {} asked to select a server before authenticating - dropping.", ctx.channel().remoteAddress()); + if(!isAllowed(session, ctx, "select a server")) return; - } byte[] payload = SrvSelect.decode(message); - log.info("Received server select request, payload: {}", Utility.byteArrayToHexString(payload)); + log.info("[conn-{}] Received server select request, payload: {}", connectionId, Utility.byteArrayToHexString(payload)); ctx.writeAndFlush(new AnsSrvSelect().createPacket()); break; } default: { - log.warn("Unknown packet! Packet: {}", Utility.byteArrayToHexString(message)); + log.warn("[conn-{}] Unknown packet! Packet: {}", connectionId, Utility.byteArrayToHexString(message)); } } } @@ -207,6 +252,38 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) // } } + /** + * Whether a post-login packet should be served. Normally that means the session authenticated; + * while the ansVerify probe is on, it does not. + * + *

The probe reaches these packets by sending {@code 33 0E 00} ({@code A2U_ansAuthUser}, + * success) directly, which the client acts on without ever sending {@code U2A_askAuthUser} - so + * the session never learns it is authenticated and the gate rejected exactly the traffic the + * probe exists to produce (see {@code CLIENT-PROTOCOL-NOTES.md} §14.2 and §14.3). + * + *

Tied to the probe flag rather than a new setting of its own because the two are inseparable + * in practice: without the probe the client never gets past verify, so it never reaches here + * unauthenticated anyway. That also means this relaxation cannot be left on by accident in + * production - {@code ANS_VERIFY_PROBE} is off there, and it already announces itself loudly. + */ + private boolean isAllowed(ClientSession session, ChannelHandlerContext ctx, String what) + { + if(session.isAuthenticated()) + return true; + + if(AuthServerConfig.isAnsVerifyProbe()) + { + log.warn("[conn-{}] PROBE: client {} asked for {} without authenticating - answering anyway.", + connectionId, ctx.channel().remoteAddress(), what); + return true; + } + + log.warn("[conn-{}] Client {} asked for {} before authenticating - dropping.", + connectionId, ctx.channel().remoteAddress(), what); + + return false; + } + @Override public void channelInactive(ChannelHandlerContext ctx) { @@ -237,7 +314,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) } else { - log.error("Unexpected error on channel {}", ctx.channel().remoteAddress(), cause); + log.error("[conn-{}] Unexpected error on channel {}", connectionId, ctx.channel().remoteAddress(), cause); } ctx.close(); diff --git a/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ServerPacketHandler.java b/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ServerPacketHandler.java index 4ccb539..6b270c1 100644 --- a/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ServerPacketHandler.java +++ b/auth-server/src/main/java/com/valiantgaming/authserver/server/handler/ServerPacketHandler.java @@ -7,6 +7,7 @@ import com.valiantgaming.authserver.network.packet.server.handler.GetServerInfo; import com.valiantgaming.authserver.network.session.client.ClientSession; import com.valiantgaming.authserver.network.session.client.ClientSessionManager; +import com.valiantgaming.authserver.network.session.server.GameServerRegistry; import com.valiantgaming.authserver.network.session.server.PendingAuthRequests; import com.valiantgaming.authserver.network.session.server.ServerSession; import com.valiantgaming.authserver.network.session.server.ServerSessionManager; @@ -87,6 +88,10 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { ServerInfo serverInfo = GetServerInfo.decode(message); log.info("Received ServerInfo response from " + ctx.channel().remoteAddress() + ": " + serverInfo); + + // Kept rather than just logged: this is the row AnsSrvList offers the client, and it is + // the only live server data auth-server has - see GameServerRegistry. + GameServerRegistry.setGameServer(serverInfo); } else if(message[1] == Protocol.S2S_ansAuthUser) {