• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

UDP Holepunching, and how GMS2 handles sockets/ports

INFERNOUX

Member
Over the course of the last several hours I've spent some time messing around with the concept of UDP holepunching. I've put together a small "master server" on my local machine that is port forwarded, and have made two clients that are on two other networks out of state. As it stands, they're all using a command line system that I've put together to just handle connecting and disconnecting. Everything is working for network connection to the server, opening sockets and ports and communicating back and forth from both clients to my location.

After i've seen that both clients are connected I attempted to set up a small holepunching script, where the server sends both clients the opposing clients IP and Port, both clients send eachother packets multiple times, a random period of time apart.

Initially setting up the socket on the client in the create event:
GML:
network_set_config(network_config_use_non_blocking_socket,1);
mysocket = network_create_socket_ext(network_socket_udp,33115);

Getting the IP and port from the server in the async networking event:
GML:
        //begin holepunching
        connection_ip = buffer_read(buffer,buffer_string);
        connection_port = buffer_read(buffer,buffer_u32);
        //send a message to the cmd to update the user
        sendnotice("Attempting holepunching from " + myip + ":" + string(myport) + " to " + connection_ip + ":" + string(connection_port));
        holepunch = 1; //variable for handling holepunching in the step event
        var buff = buffer_create(256,buffer_grow,1);
        buffer_write(buff,buffer_u16,2);
        network_send_udp(mysocket,masterip,masterport,buff,buffer_get_size(buffer));
        //send a notice to the master server that I am trying to holepunch
        buffer_delete(buff);
Sending the actual punch packets to each client in the step event:

GML:
if(holepunch){
    if(punchtimer = maxtimer){ //room_speed/2 + irandom_range(0,25);
        buff = buffer_create(256,buffer_grow,1);
        buffer_write(buff,buffer_u16,4);
        altsocket = network_create_socket_ext(network_socket_udp,35565);
        network_send_udp(mysocket,connection_ip,33115,buff,buffer_get_size(buff));
        network_send_udp(mysocket,connection_ip,35565,buff,buffer_get_size(buff));
        network_send_udp(altsocket,connection_ip,33115,buff,buffer_get_size(buff));
        network_send_udp(altsocket,connection_ip,35565,buff,buffer_get_size(buff));
        network_send_udp(mysocket,connection_ip,connection_port,buff,buffer_get_size(buff));
        network_send_udp(altsocket,connection_ip,connection_port,buff,buffer_get_size(buff));
        buffer_delete(buff);
        punchtimer = 0;
    }
    punchtimer++;
}

As you can see, im sending multiple different packet and socket combinations between the clients. In order from top to bottom the combinations are:
1. the socket used to talk to the server, and the port that is used in initializing each client,
2. the socket used to talk to the server, and the port that the server is using,
3. a new socket the client created to emulate the servers socket (to try to spoof the firewall), and the port that is used in initializing each client,
4. as number 3, but communicating through the port that the server is using
5. the socket used to talk to the server, and the actual port that the server sees from the opposing client.
6. the new socket mentioned in number 3, and the actual port that the server sees from the opposing client.

now, the strange part that ive noticed through all of this testing (and the reason for number 5 and 6), is that the outgoing port, and the incoming ports don't match. Somewhere between GMS sending out the packets, and the packets getting received by the opposing clients, the port is changing.

unknown.png

The left set of ports is what one of the machines firewalls is seeing as the senders information. the right set is the listed destination on the machine. as you can see; the listed "sender" ports do not match any of the information that I had previously set up at all. I imagine this is either a feature in GMS, or a feature in Windows to try to make security better, but for holepunching to work I need to be able to force outbound sockets to actually be through the sockets im listing in the code. However, literally none of them match up 1:1. If anyone could give me any information on what may be causing that it would be extremely helpful, and if anyone could give me any potential solutions to the problem it would really help make the last few hours of troubleshooting "worth while" for me.

Thank you for everyone's time and patience.
 
Last edited:

FrostyCat

Redemption Seeker
The last time I attempted UDP holepunching several years ago, I had to start the socket in server mode (i.e. network_create_server_raw) for it to work. That way their return ports stay constant while the hole is being punched.
 

INFERNOUX

Member
The last time I attempted UDP holepunching several years ago, I had to start the socket in server mode (i.e. network_create_server_raw) for it to work. That way their return ports stay constant while the hole is being punched.
Well, I did some research and apparently it's the modem changing the port number on it's way out not gml. I'd love to see how creating a raw server affects it. I'll test it tomorrow when I have more time to do so.

I may have to do some holepunching soon for a project so I'm keen to see more than this. For those drifting by here are some resources:


I did some reading into gmnet a few days ago, and honestly if I have to run a second server aside from just the one I'm handling in gml I don't really want it. My hope is to have it done 100% in game maker that way I can share what I figured out with the rest of the community.

Once I get all the hiccups worked out of it I'll probably be putting together a much more thorough explanation of how I got it to work.

Thank you both for your feedback.
 

Juju

Member
Well... running a mutually connectable server is the way you negotiate a connection between the two clients.That's the only way I'm aware of to get holepunching to work so I think you're outta luck if you don't want to run a server at all.

Conceivably you could get your players to manually enter each other's IP addresses, but that seems like pretty nasty UX (especially for the host who'd have to enter an IP for every client). Maybe @FrostyCat has some trick up his sleeve?
 

INFERNOUX

Member
Well... running a mutually connectable server is the way you negotiate a connection between the two clients.That's the only way I'm aware of to get holepunching to work so I think you're outta luck if you don't want to run a server at all.

Conceivably you could get your players to manually enter each other's IP addresses, but that seems like pretty nasty UX (especially for the host who'd have to enter an IP for every client). Maybe @FrostyCat has some trick up his sleeve?
Gmnet uses a Java server alongside the gml server. I meant I don't intend to use any other server other than just the gml server that I've made/I'm making. 1 server not two.

If you read into gmnet punch it is running a Java application in the background to handle the keepalive messages and the final punch. I'm trying to avoid that.
 

FrostyCat

Redemption Seeker
Conceivably you could get your players to manually enter each other's IP addresses, but that seems like pretty nasty UX (especially for the host who'd have to enter an IP for every client). Maybe @FrostyCat has some trick up his sleeve?
I don't have any tricks other than the trivial solution: The holepunching server tells the clients of available IP address-port combintions, and invites them to connect to each other.
Gmnet uses a Java server alongside the gml server. I meant I don't intend to use any other server other than just the gml server that I've made/I'm making. 1 server not two.

If you read into gmnet punch it is running a Java application in the background to handle the keepalive messages and the final punch. I'm trying to avoid that.
No, GMNet uses the Java server in place of your GML server, not in addition to. After the initial holepunch and learning of each other's IP address and port from the Java server, either client can connect to the other on a peer-to-peer basis.
 

INFERNOUX

Member
I don't have any tricks other than the trivial solution: The holepunching server tells the clients of available IP address-port combintions, and invites them to connect to each other.

No, GMNet uses the Java server in place of your GML server, not in addition to. After the initial holepunch and learning of each other's IP address and port from the Java server, either client can connect to the other on a peer-to-peer basis.
Sadly, I know next to nothing about Java, so my hope is to be able to use a gml server to handle it all. I've made quite a bit of progress, and if I'm lucky I'll have it functioning tomorrow. Fingers crossed.
 

FrostyCat

Redemption Seeker
Sadly, I know next to nothing about Java, so my hope is to be able to use a gml server to handle it all. I've made quite a bit of progress, and if I'm lucky I'll have it functioning tomorrow. Fingers crossed.
That server is already written for you, it is ready to go. You don't need to know Java to run that server, any more than your players need to know GML to play your game.
 

INFERNOUX

Member
That server is already written for you, it is ready to go. You don't need to know Java to run that server, any more than your players need to know GML to play your game.
At this point it really comes down to concept. I want to do it myself, and do it in gml entirely. Call me stubborn, but GML should be able to do it and I want to prove it.

One of the biggest issues I've had in my testing is that one of the clients I'm using is on a symmetric router which really prevents udp hole punching. In this case I'm trying to get UPnP to work as a fallback.

I genuinely believe I have udp hole punching working, I just need another asymmetric router to test it with. I'll probably post here again in about a week to confirm.
 

PNelly

Member
I have some experience in this space. You can certainly setup a hole punching implementation in pure GML, it just comes with some constraints. I implemented the multiplayer for Courtyard Broomball, which is built on a udp hole punching infrastructure that's 100% GML. It's very far from perfect (I also haven't checked in on the server in a long time), but I can assure you it can be done.

I've been out of touch for a while so I don't know if anything's changed, but when I was working with hole punching GM was only capable of building single-threaded applications, which limits you to handling low player volumes on your control server (a hard number is tough to come up with but maybe a few dozen?). If the need arises for a control server that can handle higher player volumes then you'll have to leave GML behind. In my erstwhile enthusiasm I built my own control server in Java which was a great learning exercise. If I were to do it again though I'd probably use another language because Java and GM use different byte-ordering which was a bit of pain. Java is also somewhat "uncool" these days in the programming world if you care about those kinds of things.

Another thing is that GM applications require a GPU to be available to run, so if you build a control server in GM and you want to deploy it to the cloud that more or less restricts you to using windows server, which means paying more for less performance. A custom CLI application built in a language of your choosing can better take advantage of the computing resources available and can be deployed on a cheap, GPU-free linux box.

This writeup was a fantastic resource for getting wrapped around the basic problem to be solved. It's a bit dated but all the core principles remain the same, you're tricking the NAT's of each peer-to-be into allowing one another's network traffic through. Because NAT's map outbound traffic to a different port than it originated on, and because they do this without revealing the mapping to the machines that sent the data, the only way to discover the port/ip tuples the peers need to talk to each other is to use a control server which visibility to each peers' external IP and port numbers. The control server shares this information with peers desiring to connect with one another and the hole punching can proceed.

For what it's worth I found it much easier in the end to handle most of the control server interactions over TCP, and leaving UDP alone until it was actually needed for establishing the peer connections. So information like "player abc is online", and "player xyz is hosting a game session" would be shared over TCP, but the ip/port tuple exchange and hole punches wouldn't begin until player abc informed the server of her intention to join the session hosted by player xyz.

My memory is foggy here but the only behavioral difference I can recall with the *_raw networking functions is that some header information will be removed from your packets (which you really don't need, so you might as well claw those bytes back).

I have a repository on bitbucket containing all this stuff that I had been very jealously guarding, but seeing as I haven't touched it in more than two years it might make sense to just make it available to whoever would like to learn from it. It works pretty well but definitely brings about some "I can't believe I wrote this hideous code" reactions from me.

Good luck on your project.
 

PNelly

Member
A couple thoughts on the details you've shared.

Just to be extra sure, the procedure to follow for a hole punch is this:
  1. You have Clients A and B, and Control Server S
  2. A and B transmit udp packets to S. So A -> udp via "my_socket" -> S and B -> udp via "my_socket" -> S
  3. S notes the originating port/ip address that it sees when receiving these packets. A may have opened its udp socket locally on port X, but when S receives A's message S would see some other number Y, which is the number that B needs to be aware of. Likewise, client B may have opened it's udp socket on port P, but when S receives B's message, the originating port is seen as Q, which is the number that A needs to be aware of. Obtaining the public-facing port numbers Y and Q (along with A and B's public facing IPs) is the reason we have to set all this up.
  4. S sends the external facing, originating port/ip tuple of A to B, and vice versa, the external facing, originating port/ip tuple of B to A
  5. A and B now each have the public facing ports/ips of their counterpart, and can begin the hole punch
  6. A and B each send UDP packets originating from the same port mapping they used to talk to the server, but substitute the destination with the external mappings they received from S. That is, you now have A -> udp via "my_socket" -> B using port Q and B -> udp via "my_socket" -> A using port Y
  7. If all as well A and B should receive each other's packets
Looking at your numbered list of sockets in your hole punch step, number 5 is the closest, but I'm not sure what "connection_ip" represents. For each peer it needs to be the public facing ip of their counterpart.

I'm remembering a couple gotchya's that can be obnoxious:
  • When the clients open their udp sockets, it's very important that an outbound packet gets sent every couple of seconds to maintain the port mapping that was assigned by the NAT. If too much time elapses the bridge will be closed by the NAT and a new, different port mapping will be opened on a later transmission, which puts you back on square one.
  • It's important when testing this process that A and B are actually residing behind different NAT's, with the control server residing at a publicly accessible IP. I my case I put my control server in the cloud and enlisted a friend to help me test over the phone. Changing the network topology in a way that doesn't correspond to the use case (like doing this on three different laptops on home wifi) is going to get you behavior that requires a different solution. E.g., if A and B are inside the same private network then you'll want to use a LAN focused connection solution. I think you should be alright with the setup you described in your original post with the port-forwarded control server.
 
Top