21 KiB
EverQuest II Login Server - Complete Technical Documentation
Table of Contents
- Overview
- Architecture
- Network Protocol Stack
- Encryption and Security
- Packet Structure
- Complete Login Flow
- Character Management
- World Server Communication
- Error Handling
- Database Operations
Overview
The EverQuest II Login Server is responsible for:
- Client authentication and authorization
- World server discovery and status management
- Character listing and management
- Secure handoff to world servers
- Account management and creation
Key Components
- NetConnection: Main network manager
- Client: Individual client connection handler
- ClientList: Thread-safe client container
- LWorld: World server representation
- LWorldList: World server manager
- LoginDatabase: Database interface
- EQStream: Network stream handler with encryption
Architecture
┌─────────────┐ UDP ┌──────────────┐
│ EQ2 Client ├─────────────────►│ Login Server │
└─────────────┘ └──────┬───────┘
│ TCP
▼
┌──────────────────┐
│ World Servers │
└──────────────────┘
Port Configuration
- Default Login Port: 5999 (UDP for game clients)
- Web API Port: Configurable (TCP/HTTPS)
- World Server Communication: TCP (internal)
- World Server Game Port: Dynamic per world (UDP for game clients)
Network Protocol Stack
Layer Structure
Application Layer: [Game Packets]
Presentation Layer: [PacketStruct Serialization]
Session Layer: [EQStream with RC4/CRC]
Transport Layer: [UDP Datagram Protocol]
Network Layer: [IP Protocol]
Note: EQStream implements its own reliability layer on top of UDP, providing:
- Sequence numbers for ordering
- Acknowledgments for reliability
- Retransmission for lost packets
- This gives the benefits of TCP while maintaining lower latency
EQStream Protocol
The EQStream protocol provides:
- Sequencing: Packet ordering and acknowledgment
- Fragmentation: Large packet splitting
- Compression: zlib compression for efficiency
- Encryption: RC4 stream cipher
- Integrity: CRC32 checksums
Packet Types
enum EQStreamOp {
OP_SessionRequest = 0x0001, // Initial connection
OP_SessionResponse = 0x0002, // Server accepts connection
OP_Combined = 0x0003, // Multiple packets in one
OP_SessionDisconnect = 0x0005, // Connection termination
OP_KeepAlive = 0x0006, // Connection heartbeat
OP_SessionStatRequest = 0x0007, // Statistics request
OP_SessionStatResponse = 0x0008,// Statistics response
OP_Packet = 0x0009, // Application data
OP_Fragment = 0x000d, // Fragmented packet piece
OP_Ack = 0x0015, // Acknowledgment
OP_AckFuture = 0x0016, // Future acknowledgment
OP_AckPast = 0x0017 // Past acknowledgment
};
Encryption and Security
RC4 Encryption
The login server uses RC4 stream cipher for packet encryption:
-
Key Exchange:
// Initial session setup struct SessionRequest { uint32_t unknown; // Protocol version uint32_t session_id; // Client session ID uint32_t max_length; // Max packet size }; struct SessionResponse { uint32_t session_id; // Server session ID uint32_t key; // RC4 key seed uint8_t crc_length; // CRC bytes (2) uint8_t compression; // Compression flag uint32_t unknown; // Reserved uint32_t max_length; // Max packet size };
-
RC4 Key Generation:
// Key is derived from session response void GenerateRC4Key(uint32_t key_seed) { // Initialize RC4 state with key_seed RC4_KEY encrypt_key, decrypt_key; unsigned char key_bytes[4]; memcpy(key_bytes, &key_seed, 4); // Setup encryption/decryption keys RC4_set_key(&encrypt_key, 4, key_bytes); RC4_set_key(&decrypt_key, 4, key_bytes); }
-
Packet Encryption:
void EncryptPacket(uint8_t* data, size_t length) { // Skip protocol header (2 bytes) RC4(&encrypt_key, length - 2, data + 2, data + 2); }
CRC32 Integrity Check
Every packet includes a CRC32 checksum:
struct EQProtocolPacket {
uint16_t opcode; // Operation code
uint8_t data[]; // Packet data
uint32_t crc32; // CRC32 of opcode + data
};
uint32_t CalculateCRC(const uint8_t* data, size_t length) {
// Standard CRC32 calculation
return crc32(0, data, length);
}
CRC Verification Process:
- Extract CRC from packet end
- Calculate CRC of packet data
- Compare calculated vs received CRC
- Reject packet if mismatch
Packet Structure
Base Packet Header
struct EQ2Packet {
uint16_t opcode; // Operation identifier
uint16_t sequence; // Packet sequence number
uint32_t size; // Payload size
uint8_t compressed; // Compression flag
uint8_t data[]; // Actual payload
};
Application Packet Structure
struct EQApplicationPacket {
uint16_t opcode; // Game operation code
uint32_t version; // Client version
uint8_t data[]; // Serialized data
};
Complete Login Flow
Phase 1: Connection Establishment
-
Client Initiates UDP Communication:
Client → Server: UDP Datagram with OP_SessionRequest (No TCP handshake - connectionless protocol)
-
EQStream Session:
Client → Server: OP_SessionRequest { protocol_version: 0x0003 session_id: random() max_packet_size: 512 } Server → Client: OP_SessionResponse { session_id: server_session rc4_key: generated_key crc_length: 2 compression: true max_packet_size: 512 }
-
Encryption Initialization:
- Both sides initialize RC4 with the shared key
- All subsequent packets are encrypted
Phase 2: Authentication
-
Login Request:
Client → Server: OP_LoginRequestMsg { version: client_version // e.g., 1208 username: "account_name" // EQ2_16BitString password: "hashed_password" // EQ2_16BitString unknown3: cl_eqversion // From eq2_defaults.ini }
-
Version Validation:
// Server checks version compatibility if (!EQOpcodeManager.count(GetOpcodeVersion(version))) { // Send incompatible version error SendLoginDeniedBadVersion(); return; }
-
Account Verification:
// Database lookup LoginAccount* acct = database.LoadAccount(username, password); // Check for duplicate login Client* existing = client_list.FindByLSID(acct->getLoginAccountID()); if (existing) { existing->getConnection()->SendDisconnect(); } // Update account info database.UpdateAccountIPAddress(acct->id, client_ip); database.UpdateAccountClientDataVersion(acct->id, version);
-
Login Response:
Server → Client: OP_LoginReplyMsg { account_id: account_id login_response: 0 // 0 = success sub_level: 0xFFFFFFFF // Subscription level race_flag: 0x1FFFFF // Available races class_flag: 0x7FFFFFE // Available classes expansion_flag: 0x7CFF // Enabled expansions cities_flag: 0xFF // Starting cities enabled_races: 0xFFFF // Race availability }
Phase 3: World List Request
-
Client Requests World List:
Client → Server: OP_AllWSDescRequestMsg {} // Empty request
-
Server Builds World List:
Server → Client: OP_AllWSDescReplyMsg { num_worlds: count worlds[]: { world_id: id world_name: "ServerName" world_status: 1 // 0=down, 1=up, 2=locked num_players: current max_players: maximum language: "en" recommended: false } }
-
World Status Updates:
- Server sends periodic updates (every 10 seconds)
- Updates include player counts and status changes
Phase 4: Character List
-
Character Loading:
// Server loads characters from database database.LoadCharacters(GetLoginAccount(), GetVersion());
-
Character List Response:
Server → Client: LS_CharSelectList { account_id: account_id num_characters: count characters[]: { char_id: database_id server_id: world_id name: "CharacterName" race: race_id class: class_id level: current_level zone: "ZoneName" gender: 0/1 deity: deity_id body_size: size body_age: age soga_race_type: soga_id x: position_x y: position_y z: position_z } }
Phase 5: Character Creation
-
Creation Request:
Client → Server: OP_CreateCharacterRequestMsg { server_id: target_world name: "NewCharacter" race: selected_race class: selected_class gender: 0/1 deity: selected_deity body_size: size_value body_age: age_value // Appearance data... }
-
Forward to World Server:
Login → World: ServerOP_CharacterCreate { version: client_version account_id: account_id [original_request_data] }
-
World Server Validation:
- Check name availability
- Validate race/class combination
- Check character limits
-
Creation Response:
World → Login: ServerOP_CharacterCreateResponse { success: true/false char_id: new_character_id // If successful reason: error_code // If failed } Login → Client: OP_CreateCharacterReplyMsg { response: CREATESUCCESS_REPLY or error_code name: "NewCharacter" account_id: account_id }
Phase 6: World Entry
-
Play Request:
Client → Server: OP_PlayCharacterRequestMsg { char_id: selected_character server_id: world_server // Version > 283 name: "CharacterName" // Version <= 283 }
-
World Server Handoff:
Login → World: ServerOP_UsertoWorldReq { char_id: character_id lsaccountid: account_id worldid: server_id ip_address: client_ip }
-
World Server Response:
World → Login: ServerOP_UsertoWorldResp { worldid: server_id response: 1 // 1 = success ip_address: world_ip port: world_port access_key: session_key // For authentication }
-
Client Redirect:
Login → Client: OP_PlayCharacterReplyMsg { response: 1 // Success server: "world.ip.address" port: world_port account_id: account_id access_code: session_key }
-
Client Connects to World:
- Client disconnects from login server
- Connects to world server using provided IP:port
- Authenticates using access_key
Character Management
Character Deletion
-
Delete Request:
Client → Server: OP_DeleteCharacterRequestMsg { char_id: character_id server_id: world_id name: "CharacterName" }
-
Verification:
// Verify ownership bool valid = database.VerifyDelete(account_id, char_id, name);
-
World Server Notification:
Login → World: ServerOP_DeleteCharacter { char_id: character_id account_id: account_id }
-
Delete Response:
Server → Client: OP_DeleteCharacterReplyMsg { response: 1 // 1 = success char_id: deleted_id server_id: world_id name: "CharacterName" }
World Server Communication
Inter-Server Protocol
-
World Server Registration:
World → Login: ServerOP_LSInfo { name: "WorldName" address: "public_ip" port: game_port admin_port: admin_port status: 1 // 0=down, 1=up, 2=locked players: current_count max_players: maximum }
-
Heartbeat:
World → Login: ServerOP_LSStatus { status: current_status players: current_count zones: active_zones } // Sent every 30 seconds
-
Player Updates:
World → Login: ServerOP_UsertoWorldResp { lsaccountid: account_id worldid: server_id response: status // 0 = rejected // 1 = accepted // -1 = world full // -2 = character not found // -3 = world locked }
Error Handling
Login Error Codes
enum LoginResponseCodes {
LOGIN_SUCCESS = 0,
LOGIN_BADPASS = 1, // Invalid username/password
LOGIN_BADVERSION = 6, // Client version mismatch
LOGIN_SUSPENDED = 7, // Account suspended
LOGIN_BANNED = 9, // Account banned
LOGIN_WORLDFULL = 10, // World at capacity
LOGIN_DISCONNECT = 100 // Generic disconnect
};
Play Error Codes
enum PlayResponseCodes {
PLAY_SUCCESS = 1,
PLAY_ERROR_PROBLEM = 2, // Generic error
PLAY_ERROR_WORLDFULL = 3, // World full
PLAY_ERROR_LOCKED = 4, // World locked
PLAY_ERROR_BANNED = 5, // Character banned
PLAY_ERROR_SUSPENDED = 6, // Character suspended
PLAY_ERROR_SERVER_TIMEOUT = 7, // Timeout waiting for world
PLAY_ERROR_CHAR_NOT_LOADED = 8 // Character data issue
};
Character Creation Error Codes
enum CreateResponseCodes {
CREATESUCCESS_REPLY = 1,
INVALIDRACE_REPLY = 2, // Invalid race selection
INVALIDGENDER_REPLY = 3, // Invalid gender
BADNAMELENGTH_REPLY = 9, // Name too short/long
NAMEINVALID_REPLY = 10, // Invalid characters
NAMEFILTER_REPLY = 11, // Profanity filter
NAMETAKEN_REPLY = 12, // Name already exists
OVERLOADEDSERVER_REPLY = 13, // Server overloaded
UNKNOWNERROR_REPLY = 20 // Generic error
};
Database Operations
Account Management
-
Account Loading:
SELECT id, name, password, suspended, banned FROM login_accounts WHERE name = ? AND password = SHA2(?, 256)
-
Account Creation (if enabled):
INSERT INTO login_accounts (name, password, created) VALUES (?, SHA2(?, 256), NOW())
-
IP Address Update:
UPDATE login_accounts SET last_ip = ?, last_login = NOW() WHERE id = ?
Character Operations
-
Character List:
SELECT c.id, c.name, c.server_id, c.level, c.race, c.class, c.zone, c.gender, c.deity FROM characters c WHERE c.account_id = ? AND c.deleted = 0 ORDER BY c.server_id, c.name
-
Character Creation:
INSERT INTO characters (account_id, server_id, name, race, class, gender, deity, body_size, body_age, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
-
Character Deletion:
UPDATE characters SET deleted = 1, deleted_date = NOW() WHERE id = ? AND account_id = ?
World Server Tracking
-
World Registration:
INSERT INTO login_worldservers (name, ip_address, port, status) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE ip_address = VALUES(ip_address), port = VALUES(port), status = VALUES(status)
-
Status Updates:
UPDATE login_worldservers SET status = ?, players = ?, last_update = NOW() WHERE id = ?
Security Considerations
Password Handling
- Passwords are hashed using SHA256 before transmission
- Never store plaintext passwords
- Use prepared statements to prevent SQL injection
Session Management
- Generate unique session keys for world handoff
- Session keys expire after use or timeout
- IP address validation between login and world entry
Rate Limiting
- Limit login attempts per IP
- Throttle character creation requests
- Monitor for abnormal packet patterns
Packet Validation
- Verify packet size limits
- Check sequence numbers for replay attacks
- Validate all input data ranges
Performance Optimizations
Connection Pooling
- Maintain persistent database connections
- Reuse world server connections
- Implement connection timeout and retry logic
Caching
- Cache world server status for 10 seconds
- Cache character lists until modified
- Cache opcode mappings per version
Threading Model
- Main thread handles network I/O
- Database operations on thread pool
- World server communication async
- Client processing single-threaded per client
Monitoring and Logging
Key Metrics
- Active client connections
- Login success/failure rates
- World server availability
- Database query performance
- Packet processing latency
Log Levels
enum LogLevel {
LOGIN__INFO, // General information
LOGIN__DEBUG, // Detailed debugging
LOGIN__ERROR, // Error conditions
LOGIN__WARNING, // Warning conditions
WORLD__INFO, // World server info
WORLD__ERROR, // World server errors
OPCODE__DEBUG, // Packet opcodes
INIT__INFO, // Initialization
INIT__ERROR // Init failures
};
Critical Events to Log
- Failed login attempts
- Account creation
- Character creation/deletion
- World server registration/deregistration
- Network errors and timeouts
- Database connection issues
Configuration
Main Configuration (config.json)
{
"loginconfig": {
"serverport": 5999,
"serverip": "",
"accountcreation": 1,
"expansionflag": 32463,
"citiesflag": 255,
"defaultsubscriptionlevel": -1,
"enabledraces": 65535,
"webloginaddress": "0.0.0.0",
"webloginport": 8080,
"webcertfile": "",
"webkeyfile": "",
"webkeypassword": "",
"webhardcodeuser": "admin",
"webhardcodepassword": "password"
}
}
Database Configuration
{
"database": {
"host": "localhost",
"port": 3306,
"username": "eq2login",
"password": "password",
"database": "eq2login",
"max_connections": 10,
"connection_timeout": 5
}
}
Troubleshooting
Common Issues
-
"Version Mismatch" Error:
- Client version not in opcode database
- Solution: Update opcodes.sql
-
"Cannot Connect to Login Server":
- Firewall blocking port 5999
- Server not running
- Network configuration issue
-
"World Server Unavailable":
- World server not registered
- World server crashed
- Network issue between login and world
-
Character Creation Fails:
- Name already taken
- Invalid race/class combination
- World server communication timeout
-
Cannot Enter World:
- Session key mismatch
- World server full
- Character data corruption
Debug Commands
The login server supports console commands (Linux):
l
- List all connected world serversv
- Display version informationh
- Show help menuq
- Quit server gracefully
Protocol Evolution
Version Differences
Version <= 283:
- Server ID not included in play request
- Character name used for lookup
Version <= 561:
- Auto-enter world after character creation
- Different packet structure alignment
Version >= 546:
- Enhanced error logging format
- Larger packet size support
Version >= 1208:
- New login request structure
- Additional client version field
Conclusion
The EverQuest II login server implements a sophisticated authentication and routing system with multiple layers of security and error handling. The combination of RC4 encryption, CRC32 integrity checking, and session-based authentication provides a robust framework for managing player connections and character data.
Key design principles:
- Security First: All communications encrypted
- Scalability: Support for multiple world servers
- Reliability: Comprehensive error handling
- Performance: Efficient caching and threading
- Maintainability: Clear separation of concerns
The login server acts as the gateway to the game world, ensuring only authenticated players can access their characters while maintaining the integrity and security of the game environment.