Networking

The game uses a UDP-based protocol. Unlike TCP, UDP does not guarantee that
packets arrive, or that they arrive in the correct order. For this reason,
the game protocol implements those features using the following:

* All packets have a sequence number that is utilized for reordering
* Important packets are wrapped in a SlottedMetaPacket with a subslot number
* RelatedA packets ae used to request lost packets using the subslot number
* RelatedB packets are used to confirm received SlottedMetaPackets

All of these go both ways, server <-> client. We used to only partially
implement these features: Outgoing packet bundles used SMPs and could be
resent, but not all packets were bundled and there was no logic for requesting
lost packets from the client and there was no packet reordering, which resulted
in dire consequences in the case of packet loss (zoning failures, crashes and many
other odd bugs). This patch addresses all of these issues.

* Packet bundling: Packets are now automatically bundled and sent as
  SlottedMetaPackets using a recurring timer. All manual bundling functionality
  was removed.

* Packet reordering: Incoming packets, if received out of order, are stashed and
  reordered. The maximum wait time for reordering is 20ms.

* Packet requesting: Missing SlottedMetaPackets are requested from the client.

* PacketCoding refactor: Dropped confusing packet container types. Fixes #5.

* Crypto rewrite: PSCrypto is based on a ancient buggy version of cryptopp.
  Updating to a current version was not possible because it removed the
  MD5-MAC algorithm. For more details, see Md5Mac.scala.
  This patch replaces PSCrypto with native Scala code.

* Added two new actors:
  * SocketActor: A simple typed UDP socket actor
  * MiddlewareActor: The old session pipeline greatly simplified into a
    typed actor that does most of the things mentioned above.

* Begun work on a headless client

* Fixed anniversary gun breaking stamina regen

* Resolved a few sentry errors
This commit is contained in:
Jakob Gillich 2020-09-17 17:04:06 +02:00
parent 5827204b10
commit 407429ee21
232 changed files with 2906 additions and 4385 deletions

View file

@ -0,0 +1 @@
stacktrace.app.packages=net.psforever

View file

@ -1,19 +1,21 @@
package net.psforever.server
import java.net.InetAddress
import java.net.{InetAddress, InetSocketAddress}
import java.nio.file.Paths
import java.util.Locale
import java.util.UUID.randomUUID
import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.ActorRef
import akka.actor.typed.scaladsl.Behaviors
import akka.routing.RandomPool
import akka.{actor => classic}
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
import io.sentry.Sentry
import kamon.Kamon
import net.psforever.actors.net.{LoginActor, MiddlewareActor, SocketActor}
import net.psforever.actors.session.SessionActor
import net.psforever.crypto.CryptoInterface
import net.psforever.login.psadmin.PsAdminActor
import net.psforever.login._
import net.psforever.objects.Default
@ -33,6 +35,8 @@ import org.fusesource.jansi.Ansi.Color._
import org.fusesource.jansi.Ansi._
import org.slf4j
import scopt.OParser
import akka.actor.typed.scaladsl.adapter._
import net.psforever.packet.PlanetSidePacket
object Server {
private val logger = org.log4s.getLogger
@ -90,38 +94,25 @@ object Server {
implicit val system: ActorSystem = classic.ActorSystem("PsLogin")
Default(system)
/** Create pipelines for the login and world servers
*
* The first node in the pipe is an Actor that handles the crypto for protecting packets.
* After any crypto operations have been applied or unapplied, the packets are passed on to the next
* actor in the chain. For an incoming packet, this is a player session handler. For an outgoing packet
* this is the session router, which returns the packet to the sending host.
*
* See SessionRouter.scala for a diagram
*/
val loginTemplate = List(
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]()),
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]()),
SessionPipeline("login-session-", classic.Props[LoginSessionActor]())
)
val worldTemplate = List(
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]()),
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]()),
SessionPipeline("world-session-", classic.Props[SessionActor]())
)
val netSim: Option[NetworkSimulatorParameters] = if (Config.app.development.netSim.enable) {
val params = NetworkSimulatorParameters(
Config.app.development.netSim.loss,
Config.app.development.netSim.delay.toMillis,
Config.app.development.netSim.reorderChance,
Config.app.development.netSim.reorderTime.toMillis
)
logger.warn("NetSim is active")
logger.warn(params.toString)
Some(params)
} else {
None
// typed to classic wrappers for login and session actors
val login = (ref: ActorRef[MiddlewareActor.Command], connectionId: String) => {
Behaviors.setup[PlanetSidePacket](context => {
val actor = context.actorOf(classic.Props(new LoginActor(ref, connectionId)), "login")
Behaviors.receiveMessage(message => {
actor ! message
Behaviors.same
})
})
}
val session = (ref: ActorRef[MiddlewareActor.Command], connectionId: String) => {
Behaviors.setup[PlanetSidePacket](context => {
val uuid = randomUUID().toString
val actor = context.actorOf(classic.Props(new SessionActor(ref, connectionId)), s"session-${uuid}")
Behaviors.receiveMessage(message => {
actor ! message
Behaviors.same
})
})
}
val zones = Zones.zones ++ Seq(Zone.Nowhere)
@ -137,27 +128,19 @@ object Server {
serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService](), "accountPersistence")
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager](), "propertyOverrideManager")
val loginRouter = classic.Props(new SessionRouter("Login", loginTemplate))
val worldRouter = classic.Props(new SessionRouter("World", worldTemplate))
val loginListener = system.actorOf(
classic.Props(new UdpListener(loginRouter, "login-session-router", bindAddress, Config.app.login.port, netSim)),
"login-udp-endpoint"
)
val worldListener = system.actorOf(
classic.Props(new UdpListener(worldRouter, "world-session-router", bindAddress, Config.app.world.port, netSim)),
"world-udp-endpoint"
)
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.login.port), login), "login-socket")
system.spawn(SocketActor(new InetSocketAddress(bindAddress, Config.app.world.port), session), "world-socket")
val adminListener = system.actorOf(
classic.Props(
new TcpListener(
classOf[PsAdminActor],
"net.psforever.login.psadmin-client-",
"psadmin-client-",
InetAddress.getByName(Config.app.admin.bind),
Config.app.admin.port
)
),
"net.psforever.login.psadmin-tcp-endpoint"
"psadmin-tcp-endpoint"
)
logger.info(
@ -206,31 +189,6 @@ object Server {
case Right(_) =>
}
/** Initialize the PSCrypto native library
*
* PSCrypto provides PlanetSide specific crypto that is required to communicate with it.
* It has to be distributed as a native library because there is no Scala version of the required
* cryptographic primitives (MD5MAC). See https://github.com/psforever/PSCrypto for more information.
*/
try {
CryptoInterface.initialize()
} catch {
case e: UnsatisfiedLinkError =>
logger.error("Unable to initialize " + CryptoInterface.libName)
logger.error(e)(
"This means that your PSCrypto version is out of date. Get the latest version from the README" +
" https://github.com/psforever/PSF-LoginServer#downloading-pscrypto"
)
sys.exit(1)
case e: IllegalArgumentException =>
logger.error("Unable to initialize " + CryptoInterface.libName)
logger.error(e)(
"This means that your PSCrypto version is out of date. Get the latest version from the README" +
" https://github.com/psforever/PSF-LoginServer#downloading-pscrypto"
)
sys.exit(1)
}
val builder = OParser.builder[CliConfig]
val parser = {

View file

@ -1,48 +0,0 @@
package net.psforever.pslogin
import akka.actor.{ActorRef, MDCContextAware}
import akka.testkit.TestProbe
import net.psforever.login.HelloFriend
import net.psforever.packet.{ControlPacket, GamePacket}
final case class MDCGamePacket(packet: GamePacket)
final case class MDCControlPacket(packet: ControlPacket)
class MDCTestProbe(probe: TestProbe) extends MDCContextAware {
/*
The way this test mediator works needs to be explained.
MDCContextAware objects initialize themselves in a chain of ActorRefs defined in the HelloFriend message.
As the iterator is consumed, it produces a right-neighbor (r-neighbor) that is much further along the chain.
The HelloFriend is passed to that r-neighbor and that is how subsequent neighbors are initialized and chained.
MDCContextAware objects consume and produce internal messages called MdcMsg that wrap around the payload.
Normally inaccessible from the outside, the payload is unwrapped within the standard receive PartialFunction.
By interacting with a TestProbe constructor param, information that would be concealed by MdcMsg can be polled.
The l-neighbor of the MDCContextAware is the system of the base.actor.base.ActorTest TestKit.
The r-neighbor of the MDCContextAware is this MDCTestProbe and, indirectly, the TestProbe that was interjected.
Pass l-input into the MDCContextAware itself.
The r-output is a normal message that can be polled on that TestProbe.
Pass r-input into this MDCTestProbe directly.
The l-output is an MdcMsg that can be treated just as r-output, sending it to this Actor and polling the TestProbe.
*/
private var left: ActorRef = ActorRef.noSender
def receive: Receive = {
case msg @ HelloFriend(_, _) =>
left = sender()
probe.ref ! msg
case MDCGamePacket(msg) =>
left ! msg
case MDCControlPacket(msg) =>
left ! msg
case msg =>
left ! msg
probe.ref ! msg
}
}

View file

@ -1,57 +1,28 @@
package net.psforever.pslogin
/*
import actor.base.ActorTest
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import net.psforever.login.{HelloFriend, PacketCodingActor, RawPacket}
import net.psforever.actors.net.MiddlewareActor
import net.psforever.objects.avatar.Certification
import net.psforever.packet.control.{ControlSync, MultiPacketBundle, SlottedMetaPacket}
import net.psforever.packet.{ControlPacket, GamePacket, GamePacketOpcode, PacketCoding}
import net.psforever.packet.control.{ControlSync, SlottedMetaPacket}
import net.psforever.packet.{GamePacketOpcode, PacketCoding}
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectClass
import net.psforever.types._
import scodec.bits._
import scala.concurrent.duration._
class PacketCodingActor1Test extends ActorTest {
"PacketCodingActor" should {
"construct" in {
system.actorOf(Props[PacketCodingActor](), "pca")
system.actorOf(Props[MiddlewareActor](), "pca")
//just construct without failing
}
}
}
class PacketCodingActor2Test extends ActorTest {
"PacketCodingActor" should {
"initialize (no r-neighbor)" in {
val pca: ActorRef = system.actorOf(Props[PacketCodingActor](), "pca")
within(200 millis) {
pca ! HelloFriend(135, List.empty[ActorRef].iterator)
expectNoMessage()
}
}
}
}
class PacketCodingActor3Test extends ActorTest {
"PacketCodingActor" should {
"initialize (an r-neighbor)" in {
val probe1 = TestProbe()
val probe2 = system.actorOf(Props(classOf[MDCTestProbe], probe1), "mdc-probe")
val pca: ActorRef = system.actorOf(Props[PacketCodingActor](), "pca")
val iter = List(probe2).iterator
val msg = HelloFriend(135, iter)
assert(iter.hasNext)
pca ! msg
probe1.expectMsg(msg) //pca will pass message directly; a new HelloFriend would be an unequal different object
assert(!iter.hasNext)
}
}
}
class PacketCodingActor4Test extends ActorTest {
val string_hex = RawPacket(hex"2A 9F05 D405 86")
val string_obj = ObjectAttachMessage(PlanetSideGUID(1439), PlanetSideGUID(1492), 6)
@ -577,7 +548,7 @@ class PacketCodingActorITest extends ActorTest {
probe1.receiveOne(300 milli) match {
case RawPacket(data) =>
assert(data == string_hex)
PacketCoding.DecodePacket(data).require match {
PacketCoding.decodePacket(data).require match {
case _: SlottedMetaPacket =>
assert(true)
case _ =>
@ -916,3 +887,5 @@ class PacketCodingActorLTest extends ActorTest {
object PacketCodingActorTest {
//decoy
}
*/