"Server can't bind" for host on Meta Quest 2 | UnityTransport, LAN

Hello,

I am working on a VR app on Meta Quest 2 with the current goal to add LAN multiplayer. I thought it would be good enough to go for a “basic” host/client structure.
From a practical point of view, it allowed me to re-use most of the code from the Boss Room project (thank you very much for anyone who worked on that demo project).

The target use case is to have 1 router, 3-4 players with Quests 2 connected to it, one hosts the session and the rest join.
That seemed very straightforward to me because I had no problem doing the same thing already with test projects. Meaning I have already built host/client stuffs for 2 windows computers (one is 10, the other 11) connected to the same router. Since I didn’t struggle whatsoever, I thought the step for having 3-4 Quests connected together on the same router was going to be reasonnable.

During development, I tested with 2 clients on the local host 127.0.0.1 port 7777 with ParrelSync. I have RPCs, NetworkVariables, cutsom serialization and the usual stuff you can expect with NetCode for GameObjects. And everything eventually worked fine (well, it’s localhost…).

Now, for deployment tests, I built the app and installed the Apk file on one Quest. And, to have access to the debug console, I connect the second headset to Unity via Oculus LINK (USB 3.0 cable) I connect both Quests to the router via WiFi and so they have IP of the format 192.162.x.xxx.
In game, I have a menu to let the Host and other clients give the Target IP and Port information. That means, if the Quest that is about to host has an IP of, say, 192.168.2.2 then I type that in the dedicated field (I use a default value of 7777 for the port). Then click my “Host” button. There, it crashes. To avoid app freezing, I have it restart on 127.0.0.1.

        /// <summary>
        /// Initializes host mode on this client. Call this and then other clients
        /// should connect to us!
        /// Uses the IP and port registered in <see cref="m_Settings"/>.
        /// </summary>
        public void StartHost()
        {
            Debug.Log("NetManager call to Start host");

            SetUnityTransport();
            if (NetManager.IsHost)
            {
                Debug.Log("NetManager was already running so we are shutting down before re-launching");
                StartCoroutine(Restart());
            }
            else
            {
                NetManager.StartHost();
            }

        }

        public void SetUnityTransport()
        {
            UnityTransport unityTransport = NetworkManager.Singleton.gameObject.GetComponent<UnityTransport>();
            unityTransport.SetConnectionData(m_Settings.IP, (ushort)m_Settings.port);

            Debug.Log($"Setting up transport  connection to {unityTransport.ConnectionData.Address}: {unityTransport.ConnectionData.Port}");
        }

        /// <summary>
        /// Shuts down the current session and starts a new one as a host.
        /// </summary>
        IEnumerator Restart()
        {
            Debug.Log($"Shutting down");
            NetManager.Shutdown();
            yield return new WaitForEndOfFrame();
            Debug.Log($"Restarting a host");
           
            bool isHostStarted = NetManager.StartHost();
            string msg;
            if (!isHostStarted)
            {
                 msg = "<color=red>Host couldn't be started: bind and listening to " + m_Settings.IP + ":" + m_Settings.port + " failed.\n" + "Falling back to 127.0.0.1:7777</color>";
                SetConnectionSettings(m_Settings.playerName, "127.0.0.1", 7777, m_Settings.password);
                StartHost();
                yield return new WaitForSeconds(1f);
                MultiplayerMenuManager.SetMessage(msg);
            }
        }

If it didn’t crash, I would type in the same IP and click the “Join” Button on the other Quest. Basically the equivalent of what I did for my tests on Windows machines and it worked just fine.

As you can see in the code above, the IP and Port info is set in UnityTransport Connection Data.
With the Debug.Log(), I can confirm that my in-game menu works well and that there were no mistake when the IPs and Port were typped in.
Unfortunately, the server never manages to bind whichever Quest I try with (the one with the build nor the one connected to Unity); as it says with the “Server can’t bind” Debug.LogError() that you can find in the ServerBindAndListen(NetworkEndPoint endPoint)() method that is called by StartServer() of UnityTransport.cs (All this coming from StartHost() of NetworkManager).

My assumption was that starting a host on a Quest was not going to be any different from starting a host on a Windows machine despite the obvious differences in OS. But, after about 20 hours of searching with no progress I’m starting to think that I missing something obvious. I even started to try Port Forwarding even if that makes no sense for devices that are on the same router(…right? please, I’m starting to doubt everything)

So, is there anything different about hosting sessions on Android compared to Windows? Anyone with a suggestion on how to start debuging this? UnityTransport is apparently an immutable asset so I couldn’t try to Debug.Log() and see if there are some issues with Android on that side.

Have you checked that your Android application has permission to open sockets? The manifest should have permission INTERNET (and possibly ACCESS_NETWORK_STATE too). I think Unity itself has a setting for that in the Android player settings.

Otherwise failure to bind is often caused by using the wrong address or a port that’s already in use. You can try binding to all local addresses (just pass “0.0.0.0” as the optional third parameter of SetConnectionData) and/or to a different port to see if that’s the problem.

(And yes, you’re right, there’s no need to set up port forwarding for LAN multiplayer.)

Hello ,

Thank you very much for your reply. I took some time to thoroughly look into your pointers and it eventually lead me to the solution (…or maybe it’s more a workaround?).
I’ll start describing how I solved my problem since it’s the main subject of this thread and, for the people who may be interested, I give a quick overview on how to check that you have the correct Android permissions at the end of this answer.
Also, I may have stumbled upon a bug for UnityTransport NetworkManager.StartClient but I’m not sure: it may be that I misunderstood the true meaning of the bool that is returned by that method. So, @simon-lemay-unity , if you could be kind enough to check what I wrote on that, that would be nice :slight_smile:

Solution (workaround?) to my issue
The puzzling point for me was that the code I wrote for host/client architecture was working just fine if I was using it for on Windows PCs but it kept failing on Meta Quest 2. Since the most obvious difference at first between the two platforms was their differences in OS, I thought there may be additional stuffs to enable on Android. Which is true since you need permission for INTERNET as Simon told me. I checked that (see below for how I did that), the INTERNET permission was already added and ACCESS_NETWORK_STATE didn’t make any difference, however.
So I continued by investigating the listen address of the server (the third optional parameter of SetConnectionData that Simon also suggested me) and I finally found out that my app was actually able to start a server as expected on localHost (127.0.0.1), but only at launch.

A little note (this is super basic stuff but everybody has to be a beginner first):
For those who don’t know, you can check the [netstat](https://en.wikipedia.org/wiki/Netstat) for the Quest thanks to SideQuest. You can run adb commands from SideQuest by clicking the icon on the top right corner which looks like a rectangle with an arrow. So, with your Quest connected (for me USB 3.0 cable) you run adb shell netstat -a. Other options here.
End of the note

So, if the app could launch the server on the very beginning, but was not able to change to the address I was feeding it, the problem was probably in my Restart() method.
Here is the simplified code:

IEnumerator Restart()
        {
            // we shut down the previous instance of the server
            NetworkManager.Shutdown();

            yield return new WaitForEndOfFrame();
         
            // we start the new server
            // Unitytransport Connection Data was updated before starting this Coroutine
            bool isHostStarted = NetworkManager.StartHost();

            // If something fails while Unitytransport attempts to start the Host
            if (!isHostStarted)
            {
                //Going back to 127.0.0.1
            }
        }

You may immediately spot the yield return new WaitForEndOfFrame(); because you have a fresh eye on the stuff. But I have been overlooking that line for about 30 hours by the time I saw it again…
Turns out that this Restart() was not working on PC either when I first wrote it. But I had completely forgotten about it. Adding yield return new WaitForEndOfFrame(); was mandatory if I wanted the expected behavior “ShutDown current instance then Starts the new one” to complete.
Actually, if one wants the server to restart as expected then, rather than waiting for the end of the frame which is performance dependent (hence, working on PC but not on Quest 2!!!), you have to wait that the previous instance has stopped listening. Effectively, you write yield return new WaitWhile(() => NetworkManager.IsListening); instead.
Aside overlooking that line of code, what mislead me was the LogError in the Unity console which was saying “Server couldn’t bind”. That lead me to assume that the error was in my way to handle the IP/Port data when the actual problem lied in how long it takes for UnityTransport to indeed shut down the server after calling NetworkManager.ShutDown(). What is even more misleading is that, if you attempt to launch a server with NetworkManager.StartHost before NetworkManager.ShutDown() the LogError explicitely tells you that you cannot start a new instance while one is already running.

So, @simon-lemay-unity , is this a correct way of restarting a host? Did I overlook a recommended method? Is it normal that NetworkManager.ShutDown() seems to take quite a few milliseconds to finish? Is this written somewhere in the documentation and I didn’t see it?
This list of questions may feel aggressive, but I assure you there is no animosity on my side. It’s just professional curiosity:slight_smile:

Now, NetManager.StartClient always returns True:
So, now that I dealt with starting a Host to the correct IP/Port address, I still need other players to join the host, right? Considering that, when a player starts the app, they are all their own host (at 127.0.0.1), I also have to shut down that instance and then restart a new one with the IP/Port of the new host.
yield return new WaitWhile(() => NetworkManager.IsListening); Still works wonders before attempting to start the client.
BUT, @simon-lemay-unity , NetworkManager.StartClient always returns TRUE even when I give it the wrong IP/Port information. Let’s say I tell it to connect to IP “192.”, then there is a LogError saying that it could not parse the IP (obviously) but NetworkManager.StartClient still returns TRUE. Same if I give it a complete IP/Port information that points to nothing listening (like “10.5.2.3:8080” or whatever other possibility).
Is this the expected behaviour or is it a bug? Because I checked the code and there are situations where NetworkManager.StartClient should return FALSE. If it always returns TRUE, how is one supposed to check that the client successfully connected to the host?
Maybe I should start another thread for that…?

Checking Android Permissions:
There a other accessible resources on the web about editing the Android Manifest and declaring permissions in Unity. This Unity Forum Thread is the most recent information I found about it but it actually took me a few trials with different keywords to find it on the internet. Otherwise, I found a lot of deprecated information.
So, this is a copy of the solution from the thread above but I do it for the sake of sharing.

To check the content of the Android Manifest that Unity generates automatically through Gradle when building your app, I recommend using Jadx to decompile you .apk file. It took me 30 sec to install and opened my app under 2 sec. I suppose this depends how big you app is, however. Jadx needs Java.
Then, in the “resources” folder, you can find AndroidManifest.xml.
If you want to customize the content of your manifest then, as Simon said, there is a checkbox in the Android player settings. Once checked, you will have a template available in your project. You add stuffs in there and it will be appended to the final version of the manifest that Unity generates upon build.

1 Like

Yes, that is a correct way of restarting a host. You can also check the ShutdownInProgress property instead of IsListening (although there’s nothing wrong with that either). It is normal for Shutdown to not take effect immediately. Gracefully shutting down might involve performing operations that can’t all be done synchronously when the call is made (e.g. if packets have to be flushed to the network, then the transport might need to run for another update). I’m not sure if this is documented anywhere. I’ll bring it up to the documentation team.

Good catch on StartClient returning true when provided with a malformed IP address. I’ll file a bug on our end. But regarding valid IP addresses that are simply not listening, that’s the expected behavior. We don’t want StartClient to block waiting for network traffic, so we return true as soon as the connection request is sent, without waiting for an answer. You can use the different connection callbacks to check when a connection succeeds/fails. OnClientConnectedCallback will be invoked if the connection succeeds, and OnClientDisconnectCallback if the connection fails (on a client, connection failure will also cause the manager to shut down).

Hello @simon-lemay-unity

My apologies for the delay.
Thank you very much for getting back to me, it helped a lot. And, in general, thank you very much for being active in this forum.

All the best!

1 Like

I had a similar issue and this thread helped me find a solution thanks!
Turns out my internet permission was getting stripped out of the manifest.

For anyone having a similar issue using OpenXR with meta features, there is a setting which is on by default to remove internet permissions. See this thread