From 568c968c4e00e97e370ff013fbd22da4502c0ba2 Mon Sep 17 00:00:00 2001 From: Subsonic154 Date: Tue, 10 Mar 2026 05:03:53 -0400 Subject: [PATCH 1/5] Fix failure to connect to world on routers that lack Hairpin NAT support when connecting from the same network. --- src/main/resources/application.conf | 3 +++ src/main/scala/net/psforever/actors/net/LoginActor.scala | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 20885b9e5..ba20ca933 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -4,6 +4,9 @@ bind = 127.0.0.1 # The public host name or IP address. Used to forward clients from the login # server to the world server. The default value will only allow local connections. +# If you encounter issues trying to connect to the world from machines on the same network (or the same machine when this is not the default value) +# when hosting a public server with a router that lacks Hairpin NAT support, make sure the hosts file on this system +# has the host name of the system set to resolve to it's local IP address on the network. public = 127.0.0.1 # Login server configuration diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index abfc9c900..4d0a0c0e8 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -97,6 +97,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne private val serverName: String = Config.app.world.serverName private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) + private val localHost: InetAddress = InetAddress.getLocalHost private val bcryptRounds = 12 @@ -152,7 +153,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne updateServerList() case SocketPane.NextPort(_, _, portNum) => - val address = gameTestServerAddress.getAddress.getHostAddress + val address = if (ipAddress == "127.0.0.1") ipAddress else if (ipAddress.startsWith("192.168")) localHost.getHostAddress else gameTestServerAddress.getAddress.getHostAddress log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") val response = ConnectToWorldMessage(serverName, address, portNum) context.become(idlingBehavior) @@ -165,7 +166,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse { case SocketPane.NextPort(_, _, portNum) => - val address = gameTestServerAddress.getAddress.getHostAddress + val address = if (ipAddress == "127.0.0.1") ipAddress else if (ipAddress.startsWith("192.168")) localHost.getHostAddress else gameTestServerAddress.getAddress.getHostAddress log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") val response = ConnectToWorldMessage(serverName, address, portNum) context.become(idlingBehavior) From 7056bef3837fcfeea70bb52be1c57798b2db1f62 Mon Sep 17 00:00:00 2001 From: Subsonic154 Date: Tue, 10 Mar 2026 06:34:55 -0400 Subject: [PATCH 2/5] Tweak conf comment --- src/main/resources/application.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index ba20ca933..76efc08f7 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -4,9 +4,9 @@ bind = 127.0.0.1 # The public host name or IP address. Used to forward clients from the login # server to the world server. The default value will only allow local connections. -# If you encounter issues trying to connect to the world from machines on the same network (or the same machine when this is not the default value) -# when hosting a public server with a router that lacks Hairpin NAT support, make sure the hosts file on this system -# has the host name of the system set to resolve to it's local IP address on the network. +# If you encounter issues trying to connect to the world from other machines on the same network +# when this is not the default value and you have a router that lacks Hairpin NAT support, make sure +# the hosts file on this system has the host name of the system set to resolve to it's local IP address on the network. public = 127.0.0.1 # Login server configuration From 3086fd5758951d6a9b823850e7726a66b6f6f2b8 Mon Sep 17 00:00:00 2001 From: Subsonic154 Date: Thu, 16 Apr 2026 07:29:28 -0400 Subject: [PATCH 3/5] Refactored code, cover more private IP address ranges --- src/main/resources/application.conf | 14 ++++++---- .../net/psforever/actors/net/LoginActor.scala | 28 +++++++++++++++---- .../scala/net/psforever/util/Config.scala | 1 + 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 76efc08f7..442ee8ab3 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,12 +1,14 @@ # The socket bind address for all net.psforever.services except admin. 127.0.0.1 is the -# default for local testing, for public servers use 0.0.0.0 instead. +# default for local testing, for LAN or public servers use 0.0.0.0 instead. bind = 127.0.0.1 -# The public host name or IP address. Used to forward clients from the login -# server to the world server. The default value will only allow local connections. -# If you encounter issues trying to connect to the world from other machines on the same network -# when this is not the default value and you have a router that lacks Hairpin NAT support, make sure -# the hosts file on this system has the host name of the system set to resolve to it's local IP address on the network. +# The private host name or IP address. Used to forward clients on the local network +# from the login server to the world server. The default value will only allow connections from the host machine, +# set this to the host machines private address on the local network to allow connections from other machines on it. +local = 127.0.0.1 + +# The public host name or IP address. Used to forward external clients from outside the local network +# from the login server to the world server. The default value will not allow external connections. public = 127.0.0.1 # Login server configuration diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index 4d0a0c0e8..fc7769d47 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -33,6 +33,10 @@ object LoginActor { final case class ReceptionistListing(listing: Receptionist.Listing) extends Command + private val gameTestServerAddressLocal = new InetSocketAddress(InetAddress.getByName(Config.app.local), Config.app.world.port) + private val gameTestServerAddressPublic = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) + private val localHostAddress = new InetSocketAddress("127.0.0.1", Config.app.world.port) + /** * What does a token do? * No one knows. @@ -78,6 +82,21 @@ object LoginActor { //remove color codes from the server name - look for '\\#' followed by six characters or numbers name.replaceAll("\\\\#[\\da-fA-F]{6}","") } + + /** + * Selects the appropriate host address for transfer to world server. + * This is a workaround for cases of local connections not working + * properly when using a router that lacks Hairpin NAT support. + * @param ipAddress the IP address of the connecting client in string form + * @return the appropriate host address + */ + private def selectHostAddress(ipAddress: String): InetSocketAddress = { + ipAddress.substring(0, ipAddress.indexOf(".")) match { + case "127" => localHostAddress + case "10" | "169" | "172" | "192" => gameTestServerAddressLocal + case _ => gameTestServerAddressPublic + } + } } class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) @@ -96,8 +115,6 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne private var port: Int = 0 private val serverName: String = Config.app.world.serverName - private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) - private val localHost: InetAddress = InetAddress.getLocalHost private val bcryptRounds = 12 @@ -153,8 +170,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne updateServerList() case SocketPane.NextPort(_, _, portNum) => - val address = if (ipAddress == "127.0.0.1") ipAddress else if (ipAddress.startsWith("192.168")) localHost.getHostAddress else gameTestServerAddress.getAddress.getHostAddress - log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") + val address = LoginActor.selectHostAddress(ipAddress).getAddress.getHostAddress val response = ConnectToWorldMessage(serverName, address, portNum) context.become(idlingBehavior) middlewareActor ! MiddlewareActor.Send(response) @@ -166,7 +182,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse { case SocketPane.NextPort(_, _, portNum) => - val address = if (ipAddress == "127.0.0.1") ipAddress else if (ipAddress.startsWith("192.168")) localHost.getHostAddress else gameTestServerAddress.getAddress.getHostAddress + val address = LoginActor.selectHostAddress(ipAddress).getAddress.getHostAddress log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") val response = ConnectToWorldMessage(serverName, address, portNum) context.become(idlingBehavior) @@ -499,7 +515,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne serverName, WorldStatus.Up, Config.app.world.serverType, - Vector(WorldConnectionInfo(gameTestServerAddress)), //todo ideally, ask for info from SocketPane + Vector(WorldConnectionInfo(LoginActor.selectHostAddress(ipAddress))), //todo ideally, ask for info from SocketPane PlanetSideEmpire.VS ) ) diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index ef1314248..f4c487b2a 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -84,6 +84,7 @@ object Config { case class AppConfig( bind: String, + local: String, public: String, login: LoginConfig, world: WorldConfig, From 8a786fb7b37dcb9d196cc501d22076fce70d3b31 Mon Sep 17 00:00:00 2001 From: Subsonic154 Date: Thu, 16 Apr 2026 08:01:44 -0400 Subject: [PATCH 4/5] Restore accidentally deleted log message --- src/main/scala/net/psforever/actors/net/LoginActor.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index fc7769d47..b11020c37 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -171,6 +171,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne case SocketPane.NextPort(_, _, portNum) => val address = LoginActor.selectHostAddress(ipAddress).getAddress.getHostAddress + log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") val response = ConnectToWorldMessage(serverName, address, portNum) context.become(idlingBehavior) middlewareActor ! MiddlewareActor.Send(response) From bb2acae949cf8d04e69a9a96226dbf4ef9f513c0 Mon Sep 17 00:00:00 2001 From: Subsonic154 Date: Thu, 16 Apr 2026 10:58:43 -0400 Subject: [PATCH 5/5] Better detection of loopback and local addresses --- .../scala/net/psforever/actors/net/LoginActor.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index b11020c37..bc82119bc 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -91,10 +91,15 @@ object LoginActor { * @return the appropriate host address */ private def selectHostAddress(ipAddress: String): InetSocketAddress = { - ipAddress.substring(0, ipAddress.indexOf(".")) match { - case "127" => localHostAddress - case "10" | "169" | "172" | "192" => gameTestServerAddressLocal - case _ => gameTestServerAddressPublic + val address = InetAddress.getByName(ipAddress) + if (address.isLoopbackAddress()) { + localHostAddress + } + else if (address.isSiteLocalAddress()) { + gameTestServerAddressLocal + } + else { + gameTestServerAddressPublic } } }