Author: DreTaX
Current Version: 1.2
x64 reimplementation of Facepunch's librust native library for Rust Legacy dedicated servers.
Rust Legacy's dedicated server shipped with a native Win32 DLL called librust.dll - an x86 library that handled Steamworks integration (authentication, server listings, VAC) and RCON over TCP. The original was closed-source and built against an older Steamworks SDK (v128).
With 64-bit operating environments becoming the standard and x86 support being phased out, the goal of this project is to reverse engineer the full public API surface of librust and rebuild it as a proper x64 native DLL. This lets Rust Legacy's dedicated server (assembly-csharp) run on a 64-bit process without needing legacy x86 compatibility layers.
The Steamworks SDK has been upgraded from v1.28 to v1.64 (latest) as part of this rebuild.
A longer-term goal of the project is to allow proper Steam emulation - specifically, faking a free-to-play game entry so that Rust Legacy servers can appear in Steam's server browser automatically without needing a paid license on the backend.
The game's managed code (assembly-csharp, compiled Unity C#) calls into librust via P/Invoke ([DllImport("librust")]). There are five classes in assembly-csharp that import from this library:
The main lifecycle bridge. On Awake, it attaches to or allocates a console window, registers the Unity log callback (CaptureLog) so all Debug.Log output flows through to ConsoleLog, and calls Initialize with the full command-line argument array.
On Update (called every frame on Unity's main thread), it does three things in order:
- Calls
Cycle()which pumps the Steam game server callback queue. - Calls
Console_Input()- if a command was typed in the server console, runs it viaConsoleSystem.Run. - Calls
Console_Closing()- if the native side signals a close request (CTRL+C, CTRL+BREAK, or the X button on the console window), runsquitand destroys the MonoBehaviour.
On OnDestroy, it calls Hooks.ServerShutdown() then Shutdown() to cleanly tear down the native side.
CaptureLog is the Unity log callback. Every Debug.Log, warning, error, and exception fired anywhere in the game passes through it and gets forwarded to ConsoleLog in the DLL, which handles colored console output and RCON capture.
[DllImport("librust")] public static extern int Initialize(string[] args, int numargs);
[DllImport("librust")] public static extern void Shutdown();
[DllImport("librust")] public static extern void Cycle();
[DllImport("librust")] public static extern bool Console_Closing();
[DllImport("librust")] public static extern IntPtr Console_Input();
[DllImport("librust")] public static extern void ConsoleLog(string log, string trace, int type);Handles all Steam game server logic. Init() calls Steam_ServerStartup with the listen port and protocol version (1069). If that fails, the process quits. On success it allocates and pins the two managed delegates (funcUserAuth, funcUserGroup) via GCHandle to prevent garbage collection, then registers them as callbacks. It also fetches the server's SteamID and public IP immediately after init.
StartUserAuth pins the auth ticket byte array and calls SteamServer_BeginAuthSession. The result string is checked against "ok" to decide whether to accept the connection.
OnPlayerCountChanged pushes current player count, server name, map, and tags to Steam (which updates the server browser listing) and also updates the console window title.
[DllImport("librust")] public static extern bool Steam_ServerStartup(int port, int protocol);
[DllImport("librust")] public static extern void Steam_ServerShutdown();
[DllImport("librust")] public static extern void Steam_UpdateServer(int maxplayers, int icurrentplayers, string strServerName, string strMapName, string strTags);
[DllImport("librust")] public static extern IntPtr SteamServer_BeginAuthSession(IntPtr pData, int iDataSize, ulong iUserID);
[DllImport("librust")] public static extern void SteamServer_UserLeave(ulong iUserID);
[DllImport("librust")] public static extern bool SteamServer_UserGroupStatus(ulong iUserID, ulong iGroupID);
[DllImport("librust")] public static extern void SteamServer_SetCallback_UserAuth(funcUserAuth fnc);
[DllImport("librust")] public static extern void SteamServer_SetCallback_UserGroup(funcUserGroup fnc);
[DllImport("librust")] public static extern ulong SteamServer_GetSteamID();
[DllImport("librust")] public static extern uint SteamServer_GetPublicIP();
[DllImport("librust")] public static extern void SetTitleOfConsole(string log);The main server console command class. Contains Console_AllowClose, which the game calls during its shutdown sequence to unblock the native side's console control handler and allow the process to exit.
[DllImport("librust")] public static extern void Console_AllowClose();The original x86 build may have had a watchdog thread that detected server freezes; as of v1.2 this is a fully functional implementation rather than a no-op stub. FreezeMonitor_On arms a native watchdog thread that tracks a heartbeat from the main thread; if it stops ticking for longer than the configured timeout, the watchdog collects diagnostics (native + managed call stacks), optionally writes a minidump, and can terminate the process after a configurable grace period.
[DllImport("librust")] private static extern void FreezeMonitor_On();
[DllImport("librust")] private static extern void FreezeMonitor_Off();See FreezeMonitor (Watchdog) below for full details.
FreezeMonitor_SetCallback and FreezeMonitor_SetContext are brand new exports added in v1.2. They did not exist in the original x86 librust.dll and are not part of the original 21-export surface - they are purely additive hooks that let managed code receive freeze/dump/recovery events and tag the current hook/context so freeze reports point at the offending call.
[DllImport("librust")] private static extern void FreezeMonitor_SetCallback(FreezeEventDelegate fnc);
[DllImport("librust")] private static extern void FreezeMonitor_SetContext(string context, bool record);See FreezeMonitor (Watchdog) below for full details.
Sets up the RCON callback pipeline. Setup() allocates and pins two managed delegates via GCHandle - one for password auth, one for command dispatch - and passes them to RCON_SetupCallbacks. The native side wires these into the rconpp TCP server. RunCommand executes incoming RCON commands synchronously via ConsoleSystem.Run on whatever thread calls it (the rconpp client thread in practice - Loom is not used).
[DllImport("librust")] public static extern void RCON_SetupCallbacks(rconFuncAuth auth, rconFuncCommand command);Every function imported by assembly-csharp has been implemented. Full list of the original 21 exports that existed in the x86 librust.dll:
| Export | Status | Notes |
|---|---|---|
Initialize |
✅ | Console setup, WSA init, cfg parsing, arg parsing, starts input thread |
Shutdown |
✅ | Stops input thread, destroys RCON server, WSA cleanup |
Cycle |
✅ | Pumps Steam callbacks and processes any pending RCON command on the main thread |
Console_Closing |
✅ | Returns close request flag; only true after Steam_ServerStartup succeeds |
Console_AllowClose |
✅ | Grants the blocked close handler permission to exit |
Console_Input |
✅ | Pops next command string from the input queue |
ConsoleLog |
✅ | Colored console output; routed through an async writer queue so it never blocks the caller |
SetTitleOfConsole |
✅ | UTF-8 → wide char, calls SetConsoleTitleW |
FreezeMonitor_On |
✅ | Arms the freeze watchdog and starts the heartbeat |
FreezeMonitor_Off |
✅ | Disarms the freeze watchdog |
RCON_SetupCallbacks |
✅ | Functional via custom Python RCON client; see note below |
Steam_ServerStartup |
✅ | SteamGameServer_Init with eServerModeAuthenticationAndSecure |
Steam_ServerShutdown |
✅ | Cleans up callbacks, calls SteamGameServer_Shutdown |
Steam_UpdateServer |
✅ | Pushes max players, map, name, and tags to Steam |
SteamServer_SetCallback_UserAuth |
✅ | Stores the managed delegate pointer |
SteamServer_SetCallback_UserGroup |
✅ | Stores the managed delegate pointer |
SteamServer_BeginAuthSession |
✅ | Calls BeginAuthSession, returns string status |
SteamServer_UserGroupStatus |
✅ | Calls RequestUserGroupStatus |
SteamServer_UserLeave |
✅ | Calls EndAuthSession |
SteamServer_GetSteamID |
✅ | Returns server SteamID as uint64 |
SteamServer_GetPublicIP |
✅ | Returns public IP as uint32 |
All 21 original exports are covered.
These 2 exports are brand new additions introduced in v1.2 - they have no x86 counterpart and are not part of the original 21-export surface listed above:
| Export | Status | Notes |
|---|---|---|
FreezeMonitor_SetCallback |
✅ (new in v1.2) | Registers a managed callback for freeze/dump/recovery events |
FreezeMonitor_SetContext |
✅ (new in v1.2) | Tags the current hook/context so freeze reports can point at the stuck call |
21 original exports + 2 new exports added in v1.2 = 23 total exports covered.
Initialize accepts -port and -cfg on the command line.
When -cfg <filename> is passed, the file is resolved relative to the executable directory and parsed. Supported keys:
server.port 29015
rcon.port 29016
rcon.password "yourpassword"
freeze.timeout 10
freeze.dumpafter 30
freeze.killafter 0
freeze.maxdumps 3
freeze.fulldump 0
Lines starting with # are treated as comments. Values may be optionally quoted.
If rcon.port is not specified, it defaults to server.port + 1.
The freeze.* keys configure the FreezeMonitor watchdog; see that section for details on each one.
Previously all console output (std::cout) was written directly from whatever thread called ConsoleLog/ConsolePrint, including Unity's main thread. On Windows, selecting text in a console window (or holding a mouse click on it) with QuickEdit mode enabled pauses the console's output buffer - which blocked the writing thread until the user let go or pressed a key. Since most log output ultimately came from the main thread, a single accidental click on the server console window would freeze the entire server.
As of v1.2, console output goes through a lock-free-ish producer/consumer queue (ConsolePrint → g_ConsoleQueue) that is drained by a dedicated ConsoleWriterThread. Callers only push a string and a color onto the queue and return immediately; only the writer thread ever blocks on the console handle. If the writer thread is paused by QuickEdit, the queue simply buffers lines (up to kConsoleQueueMax, with the oldest dropped and reported once output resumes) instead of stalling the game loop or RCON.
FreezeMonitor_On/FreezeMonitor_Off are no longer stubs. When armed, a background watchdog thread (FreezeWatchdogWorker) monitors a heartbeat timestamp that the managed side is expected to refresh every frame. If the heartbeat stops advancing for longer than freeze.timeout seconds, the server is considered frozen and the watchdog:
- Creates a
FreezeDumpsfolder next to the server executable and starts a timestamped report (freeze_<timestamp>.txt). - Periodically suspends and samples the main thread's call stack (native frames resolved via
dbghelp/SymFromAddr, managed frames resolved via the Mono JIT API when available) and appends them to the report, so you can see exactly where the server is stuck and whether it's stuck at the same spot. - Writes a minidump (
freeze_<timestamp>.dmp) once the freeze has lastedfreeze.dumpafterseconds, up tofreeze.maxdumpsdumps per run (setfreeze.fulldumpto capture full process memory instead of a lighter dump). - Optionally force-kills the process once the freeze has lasted
freeze.killafterseconds (0disables auto-kill), so an orchestrator/watchdog script can restart the server instead of letting it hang forever. - Reports recovery once the heartbeat resumes, including the total freeze duration.
All diagnostics work (stack walking, symbol loading, minidump writing) happens on a separate FreezeDiagWorker thread so it never adds load to, or further blocks, the frozen main thread.
Two additional exports support this:
FreezeMonitor_SetCallback(fnc)lets managed code subscribe to freeze lifecycle events (Detected,Dumped,Recovered) with the elapsed seconds and a text description, e.g. to post a Discord/RCON alert.FreezeMonitor_SetContext(context, record)lets managed hooks announce "I'm about to run<context>" right before invoking a plugin hook. If the main thread then freezes, the report includes this context and a short history of recently-entered contexts, making it much easier to pin a freeze on a specific plugin hook.
Config keys (see Configuration):
| Key | Default | Meaning |
|---|---|---|
freeze.timeout |
10 |
Seconds without a heartbeat before the server is considered frozen |
freeze.dumpafter |
30 |
Seconds frozen before a minidump is written (0 disables dumping) |
freeze.killafter |
0 |
Seconds frozen before the process is force-killed (0 disables auto-kill) |
freeze.maxdumps |
3 |
Maximum number of minidumps written per freeze episode |
freeze.fulldump |
0 |
1 to capture full process memory in the minidump instead of a lighter dump |
RCON uses a customized build of rconpp (included in the repo) over Source RCON TCP. Several fixes were made to rconpp to work correctly:
- Heartbeat packets (
id=-1) are completely disabled - the original librust never sent them and they corrupt the client's packet stream. recvis looped on the packet body until all bytes are read, handling TCP fragmentation.- Clean disconnect (
recvreturning 0) is properly detected and triggersdisconnect_client. - RCON commands are executed on Unity's main thread via
Cycle()using a condition variable, ensuringDebug.Logfires the registered log callback synchronously and output is captured correctly.
RustAdmin is not compatible with this implementation. Use legacy_rcon.py or you can build your own with librust_rcontest.
I found It a waste of time to try to make RustAdmin work.
Due to RustAdmin's non-standard internal state machine, a dedicated standalone RCON client (legacy_rcon.py) is provided. It has a graphical UI, parses status output into a player table, and streams console output live.
- Python 3.x
- Dependencies:
pip install customtkinter pyinstaller
To package the tool into a portable, standalone Windows .exe application:
pyinstaller --noconsole --onefile legacy_rcon.py
Output is in the dist directory.
- Steamworks SDK 1.64 -
steam/steam_gameserver.h - rconpp - TCP RCON server library (customized, included in repo)
- Windows SDK
- MSVC x64 build toolchain
Compile as a 64-bit DLL (x64) targeting Windows. Link against steam_api64.lib from the Steamworks SDK. The output must be named librust.dll and placed in the Rust Legacy dedicated server Plugins directory.
- Console click/freeze fix: console output is now queued and written by a dedicated background thread, so selecting text or clicking in the console window (QuickEdit) no longer freezes the server loop or RCON. See Console Output (non-blocking).
- FreezeMonitor implemented:
FreezeMonitor_On/FreezeMonitor_Offare real now - a watchdog thread detects a frozen main thread, captures native + managed call stacks, writes minidumps, and can auto-kill a hung server. See FreezeMonitor (Watchdog). - Added
FreezeMonitor_SetCallbackandFreezeMonitor_SetContextexports for managed-side freeze alerts and hook-level context tagging. - Added
freeze.timeout,freeze.dumpafter,freeze.killafter,freeze.maxdumps, andfreeze.fulldumpconfig keys.