How to use dtls after starting a NetworkedMultiplayerENet server

Godot Version

3.5.3

Question

Hi, I was wondering if there is a way to switch to using dtls after a peer connects without dtls using NetworkedMultiplayerENet. Something like take_connection method in DTLSServer (DTLSServer — Godot Engine (3.5) documentation in English), but for NetworkedMultiplayerENet.

Long version:

In the game I’m making, I want one player to act as a server, while others connect to it. The server would generate a key and a certificate, and when a client connects, it would send the certificate back to the client. I understand that this is a potential vulnerability, because the certificate wouldn’t be signed, and the connection isn’t encrypted yet. However, that will only happen once per client in the “lobby” phase. Also, the functions called by rpc will just pass values using signals, so it shouldn’t affect anything outside the game itself.

Several approaches come to mind, none of which does exactly what I want:

  1. Wait for all clients to connect first, and then use dtls (not sure if it is even possible, and even if it is some clients may not receive request, while others already switched to dtls, so server would be unable to communicate with all clients at that time).

  2. Use two servers (and two clients) per player - one without dtls, which would be used only to send the certificate, and the other with dtls. This also may not be possible, because it requires two network peers running at the same time. It would also require opening two ports instead of one.

  3. Don’t use dtls, but encrypt messages after public keys are exchanged. After reading about various ways to encrypt messages, this approach seems doable. However, only messages would be encrypted. If I understand correctly, function names in rpc calls and connection signals would still be unencrypted.

I would prefer to use NetworkedMultiplayerENet instead of UDPServer and DTLSServer since it has many useful features for managing connections.

Come to 4.3 enet has a dlts feature.

It’s the same in 3.5.3. The problem is that clients don’t have the certificate when they connect. The reason is that any player can be a server, so it will generate a new key and certificate before the server starts. Then, that certificate needs to be somehow transmitted to all clients.

Yea do the Asymmetric encryption

There is some unencrypted talking first, but once the keys are passed you can guarantee any further secrets are encrypted. Then dlts takes over.

Well, I can only encrypt messages I send. So it will still use unencrypted rpc calls, just with encrypted arguments. Also, connection signals will (I guess) also be unencrypted.

Ah, 3.5 doesnt have the SceneMultiplayer class with authenticating peers behavior.

Yea you will need to do it out of band.

Then I guess the best I can do is to try to implement the second option from the original post. Two servers: one without dtls to be used to send certificate, and another with dtls.

Are you going to do a two branch MultaplayerApi?

I am not sure about implementation yet. If I understand correctly, SceneTree can have only one network peer. Maybe I can set another one using Node.custom_multiplayer property. In the docs MultiplayerAPI — Godot Engine (3.5) documentation in English it says that setting this property can effectively allow me to run both client and server in the same scene. However, I need two servers, or two clients. I’ll need to test it first. I only need both servers running while clients are being connected to use one in order to transmit the certificate for the other. After that, I plan to disconnect the server without dtls. If there is another way to achieve this, I would really like to know.

Oh, darn another thing that godot 4 has…

I guess you could poll one of the enet instances manually but that seems more work then necessary. You would probably have to use the send raw packet, as i dont know how you would configure an rpc with a standalone MultiplayerAPI thats not in a scenetree unless you spin up an entire scene tree.

1 Like

I’ll try to implement it in the next couple of days and will post here how it went. Thank you for your input!

1 Like

Edit: this is unnecessary for what I need (see post 13).

Original post:

After some trial and error, I think I managed to do it. The more robust version should include additional checks to verify things like checking that the correct peer made an rpc call, and maybe even encrypting the message on the unencrypted connection (when the certificate is being sent). However, for simplicity, I’ll post just the basic implementation. I decided to make two AutoLoads, for the two connections. They are called MultiplayerAutoLoad1 and MultiplayerAutoLoad2. If a player is acting as a server, then peers in both of those AutoLoads will be servers, and if it is a client, then both will be clients connecting to corresponding servers. Connections on MultiplayerAutoLoad1 use dtls, while connections on the other don’t. Clients would first connect to the second (MultiplayerAutoLoad2) server, where the connection doesn’t use dtls.
Let’s name servers server1 and server2, and clients client1 and client2. A player will either have both server1 and server2, or both client1 and client2. Here is the sequence when a client tries to connect to the server:

  1. Client2 tries to connect to server2 (no dtls).
  2. When client2 receives connected_to_server signal, it requests the certificate for server1.
  3. Server2 gets that request, and sends the certificate to client2.
  4. When client2 receives the certificate, it sends it to MultiplayerAutoLoad1 and tells it to start client1.
  5. Client1 tries to connect to server1 (using dtls).
  6. When client1 receives connected_to_server signal, it tells client2 to disconnect from server2.

The only connection left is between server1 and client1, which uses dtls. Here is the code:

MultiplayerAutoLoad1

extends Node

var crypto := Crypto.new()
var key := CryptoKey.new()
var cert := X509Certificate.new()
var peer1 := NetworkedMultiplayerENet.new()
var ip := "127.0.0.1"
var port1 := 9876
var max_players := 10



func _ready():
	# Connecting signals; err can be used to verify that everything is connected correctly.
	var err := get_tree().connect("network_peer_connected", self, "playerConnected1")
	err += get_tree().connect("network_peer_disconnected", self, "playerDisconnected1")
	err += get_tree().connect("connected_to_server", self, "connectedOk1")
	err += get_tree().connect("connection_failed", self, "connectedFail1")
	err += get_tree().connect("server_disconnected", self, "serverDisconnected1")
	print(err)


func createServer():
	key = crypto.generate_rsa(4096)
	cert = crypto.generate_self_signed_certificate(key)
	peer1.dtls_verify = false
	peer1.use_dtls = true
	peer1.set_dtls_key(key)
	peer1.set_dtls_certificate(cert)
	var err := peer1.create_server(port1, max_players)
	print(err)
	get_tree().set_network_peer(peer1)


func connectToServer1(server_cert: X509Certificate):
	peer1.dtls_verify = false
	peer1.use_dtls = true
	peer1.set_dtls_certificate(server_cert)
	var err := peer1.create_client(ip, port1)
	print(err)
	get_tree().set_network_peer(peer1)


func getCertificate() -> X509Certificate:
	return cert


func playerConnected1(id: int):
	print("Client %d connected to server 1." % id)


func playerDisconnected1(id: int):
	print("Client %d disconnected from server 1." % id)


func connectedOk1():
	print("Connected to server 1.")
	MultiplayerAutoLoad2.disconnectFromServer2()


func connectedFail1():
	print("Failed to connect to server 1.")


func serverDisconnected1():
	print("Server 1 disconnected.")

And MultiplayerAutoLoad2

extends Node

var peer2 := NetworkedMultiplayerENet.new()
var ip := "127.0.0.1"
var port2 := 9877
var max_players := 10
var is_server := false


func _ready():
	# Creating new MultiplayerAPI to use for another connection
	custom_multiplayer = MultiplayerAPI.new()
	custom_multiplayer.set_root_node(self)
	var err := custom_multiplayer.connect("network_peer_connected", self, "playerConnected2")
	err += custom_multiplayer.connect("network_peer_disconnected", self, "playerDisconnected2")
	err += custom_multiplayer.connect("connected_to_server", self, "connectedOk2")
	err += custom_multiplayer.connect("connection_failed", self, "connectedFail2")
	err += custom_multiplayer.connect("server_disconnected", self, "serverDisconnected2")
	print(err)


# This is necessary; otherwise, custom multiplayer won't work.
func _process(_delta):
	if custom_multiplayer != null && custom_multiplayer.has_network_peer():
		custom_multiplayer.poll()


func createServer():
	var err := peer2.create_server(port2, max_players)
	print(err)
	custom_multiplayer.network_peer = peer2
	is_server = true


func connectToServer2():
	var err := peer2.create_client(ip, port2)
	print(err)
	custom_multiplayer.network_peer = peer2
	is_server = false


func disconnectFromServer2():
	if (!is_server):
		custom_multiplayer.network_peer = null


remote func requestCertificate():
	rpc_id(custom_multiplayer.get_rpc_sender_id(), "server1Certificate", var2bytes(MultiplayerAutoLoad1.getCertificate(), true))


remote func server1Certificate(cert_bytes: PoolByteArray):
	if (custom_multiplayer.get_rpc_sender_id() != 1):
		return
	
	var server_cert: X509Certificate = bytes2var(cert_bytes, true) as X509Certificate
	if (server_cert != null):
		print("Certificate received.")
		MultiplayerAutoLoad1.connectToServer1(server_cert)


func playerConnected2(id: int):
	print("Client %d connected to server 2." % id)


func playerDisconnected2(id: int):
	print("Client %d disconnected from server 2." % id)


func connectedOk2():
	print("Connected to server 2.")
	rpc_id(1, "requestCertificate")


func connectedFail2():
	print("Failed to connect to server 2.")


func serverDisconnected2():
	print("Server 2 disconnected.")

Thank you to pennyloafers for helping me decide what approach to take.

After further testing, the solution from post 12 seems to be unnecessary (although it can still be used to have multiple servers or multiple clients). When dtls is used, the certificate will automatically be sent to the client, just dtls_verify needs to be set to false on the client side (since the certificate is self-signed). I checked the packets that are being sent, and when dtls is enabled, the same message sent multiple times is encrypted differently each time.