Proxying Rainbow Six LAN for WAN with .NET
"Go go go!" has been one of those quotes that stuck around with me for a while. Not because it is some linguistic masterpiece but because of both the frequency and way the line was delivered in Rainbow Six Vegas. For a game that came out back in 2006, with its sequel out in 2008, it is still pretty enjoyable. Having a nice mix of stealth and action, you can approach each section of the map from different angles to try different strategies. I don't think it is Game of the Year material or anything but you can have some good solid fun with it.
I mainly play the sequel now, Rainbow Six Vegas 2, which has both an online mode and a LAN mode for both match making and a mode called "Terrorist Hunt". I never played much of the online mode though it doesn't matter now anyway as Ubisoft shutdown the online servers so even if I wanted to, I can't. LAN mode is where I've been having most of my fun with the game though with times changing with tools like Discord and it being easier to organize playing remotely than in person, it was important to try and get this game working over a VPN too.
Initially we tried a fairly basic VPN to each other's home network. We could access each other's systems but the game wouldn't find the running server in the other's network. Unfortunately the game doesn't have a "Connect to IP" functionality so we couldn't help the game out. We tried again more recently with Tailscale as a more simplified way of connecting our networks but to no avail.
I'm no stranger to writing code to work around buggy behaviour in old games and with both my friend and I being programmers, we thought we'd dig a bit into what exactly is the game and server doing for the communication and maybe we could help it along with a bit of code.
Packet Inspection with Wireshark
Wireshark is a great tool for analyzing network traffic. I only scratch the surface of the functionality of it but even then, it really helps quite a bit with understanding what is going on.
I figured Rainbow Six Vegas 2 was either listening for a broadcast packet from the server or was perhaps sending one of its own when we refresh the list of servers available. I would have typically thought it was the former from the point of view that "servers announce themselves to clients" but that isn't the case at all.
There are 4 packets that serve as part of this handshake between the game (client) and the server:
- Client sends a packet to 255.255.255.255 to announce itself
- Server responds back to the announced client
- Client sends some sort of acknowledgement
- Server responds one final time with some acknowledgement
My friend and I figure it is likely that broadcast packet isn't getting from the client to the game server.
At this point, I thought the best strategy was to identify the data in the transmission but it didn't take long till I hit roadblocks there.
Rainbow Six Vegas 2 was built with Unreal Engine 3 and there is some pretty decent documentation available. Unlike newer versions of the engine, the code itself isn't available without forking over a large amount of money. What I was looking for specifically was for the wire protocol as I figured the game probably wasn't implementing something custom.
There is an official page for the networking overview of UE3 which includes links to jump to sections about the network driver implementation and the wire protocol but... the links don't work. The actual content of the page cuts off just before those sections for some reason - guessing it is intentional but kinda frustrating for what I'm wanting to do.
I thought I'd instead manually analyze the data to see what specifically is being transferred.
I captured the first packet, the broadcast from the client to the server, multiple different times which looked like this (each row is a version of the data in the order I tried them).
34e06d2afb5849af04842c614100
3448fcb5f3598849ec852c614100
34bc213e2e217f5868842c614100
34b8040d9d93566399842c614100
We can see the first byte is the same, the next 9 bytes change each time and the final 8 bytes don't change. I figured maybe there is a timestamp or maybe game version but even with using ImHex, a fantastic tool to help decode binary data, I couldn't really work any of the bytes out. There's a chance maybe the random data is just a nonce to prevent responses from servers being mixed up.
Maybe I'd have more luck with the packets so looking at the second packet, the one from the server back to the game, I recorded the bytes from multiple attempts (again, each row is a version of the data).
36e06d2afb5849af04842c614160a7936d748cea5960fc2198b550a8f5efb22c41d8099a308051013e0418c8a6fe63041800fe01fe010000fe01fe010002c2ccc47064ca646860c468ca72c470c26e60cac6c8ccca62627070c672c6707200a8eae4dccae4d40008000200000000000000000008000000000000009479c51efeffffff03000000000000008051013e9c0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000600000000000000080000000200000002000000ca00000000000000feffffff150000000000000004000000000008000000000002000000000000000000000010000000a8eae4dccae4d40000000000000000000000
3648fcb5f3598849ec852c614160a7936d748cea5960fc2198b550a8f5efb22c41d8099a308051013e0418c8a6fe63041800fe01fe010000fe01fe010002c2ccc47064ca646860c468ca72c470c26e60cac6c8ccca62627070c672c6707200a8eae4dccae4d40008000200000000000000000008000000000000009479c51efeffffff03000000000000008051013e9c0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000600000000000000080000000200000002000000ca00000000000000feffffff150000000000000004000000000008000000000002000000000000000000000010000000a8eae4dccae4d40000000000000000000000
36bc213e2e217f5868842c614160a7936d748cea5960fc2198b550a8f5efb22c41d8099a308051013e0418c8a6fe63041800fe01fe010000fe01fe010002c2ccc47064ca646860c468ca72c470c26e60cac6c8ccca62627070c672c6707200a8eae4dccae4d40008000200000000000000000008000000000000009479c51efeffffff03000000000000008051013e9c0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000600000000000000080000000200000002000000ca00000000000000feffffff150000000000000004000000000008000000000002000000000000000000000010000000a8eae4dccae4d40000000000000000000000
36b8040d9d93566399842c614160a7936d748cea5960fc2198b550a8f5efb22c41d8099a308051013e0418c8a6fe63041800fe01fe010000fe01fe010002c2ccc47064ca646860c468ca72c470c26e60cac6c8ccca62627070c672c6707200a8eae4dccae4d40008000200000000000000000008000000000000009479c51efeffffff03000000000000008051013e9c0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000600000000000000080000000200000002000000ca00000000000000feffffff150000000000000004000000000008000000000002000000000000000000000010000000a8eae4dccae4d40000000000000000000000
There does actually seem to be a pattern between these responses and the original requests. The first byte might be different from request and response (maybe to signal "client" vs "server") with the next several bytes matching the original request, backing up my thought about it being a nonce. The rest of the data I found to be a bit hit-or-miss in decoding it - it is static for each attempt but nothing was actually changing the state of the game server either.
I was hitting a dead end but thought I'd just look at these final packets between the client and the server. The client sends the the following in each of my attempts.
0100
0100
0100
0100
The server then responds in kind.
0101
0101
0101
0101
It is some sort of acknowledgement but I wasn't going to be able to figure it out but that's when it kinda hit me - I don't need to understand the data, just make sure it gets to the right place.
Earlier I mentioned I think it is the broadcast packet, that first one from the client looking for servers, that is the problem so if I can get that to the server maybe everything will Just Work�.
Proxying the Packets
Of the 4 packet exchange, the client broadcasts from a random port to a specific port (45000) and the server will first respond to whatever port sent the broadcast. The last 2 packets are on a different port (11120) for both sending and receiving.
To proxy the packets then, I need a program on the client machine to listen on port 45000 for the broadcast and send it directly to the server. Because the server will respond back to whatever port sent the "broadcast", that means my application will get the request so I need to then respond to the real broadcast on the original port too.
In .NET, it is relatively simple to work with sockets in a case like this as I'm not really needing to do anything too fancy. I threw something together in LINQPad to see if the idea would pan out.
var serverAddress = new IPEndPoint(IPAddress.Parse("the-ip-address"), 45000);
using var proxiedServer = new Socket(serverAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
var interceptAddress = new IPEndPoint(IPAddress.Any, 45000);
using var clientIntercept = new Socket(interceptAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
clientIntercept.Bind(interceptAddress);
var buffer = new byte[512];
while (true)
{
var broadcastIntercept = await clientIntercept.ReceiveFromAsync(buffer, SocketFlags.None, interceptAddress);
Console.WriteLine("Client Broadcast - {0} Bytes", broadcastIntercept.ReceivedBytes);
Console.WriteLine(Convert.ToHexString(buffer.AsSpan().Slice(0, broadcastIntercept.ReceivedBytes)));
await proxiedServer.SendToAsync(buffer.AsMemory().Slice(0, broadcastIntercept.ReceivedBytes), serverAddress);
Console.WriteLine("Forwarded to Server");
var serverResponse = await proxiedServer.ReceiveFromAsync(buffer, serverAddress);
Console.WriteLine("Server Response - {0} Bytes", serverResponse.ReceivedBytes);
Console.WriteLine(Convert.ToHexString(buffer.AsSpan().Slice(0, serverResponse.ReceivedBytes)));
await clientIntercept.SendToAsync(buffer.AsMemory().Slice(0, serverResponse.ReceivedBytes), broadcastIntercept.RemoteEndPoint);
Console.WriteLine("Forwarded to Client");
}
And, well, I think this screenshot says everything...
This test was done from my laptop, going through a mobile hotspot, to my desktop via Tailscale.
From what I can tell then, simply getting the initial two packets between the game client and the server allowed the game's net code to take over from there. I didn't check whether those final two packets actually got to the right spot as I was happy enough the game worked.
The only thing missing, and what could be related to those two packets, was that there was no ping being displayed in the game's server listing. That didn't matter to me though, the game worked and I could play.
Wrapping Up
I was both surprised and confused it was "that easy" given my diving into the packets themselves. For all I knew, I'd need to proxy all the game's networking packets but that wasn't the case at all - just those first 2 packets was enough to let the game do its thing. It seems to trick the game enough to think the client/server are local when they actually aren't.
I packaged up the code and published it to GitHub as "Networked Vegas 2", a simple tool to get that broadcast packet to a specific server. Right now I'm assuming this problem I hit might be unique to Rainbow Six Vegas 2 however in the released executable, it supports specifying a custom port in case it is different for other games.
I'm not intending to support other games but who knows, maybe this specific client/server communication is common for UE3 games and might help others play their LAN games over the internet.