New session pipeline and able to see server selection

This commit is contained in:
Chord 2016-04-24 19:06:17 -04:00
parent 0d986bcb29
commit 53488613d6
5 changed files with 411 additions and 253 deletions

View file

@ -0,0 +1,302 @@
// Copyright (c) 2016 PSForever.net to present
import java.net.{InetAddress, InetSocketAddress}
import akka.actor.{ActorRef, Identify, Actor, ActorLogging}
import psforever.crypto.CryptoInterface.{CryptoStateWithMAC, CryptoState}
import psforever.crypto.CryptoInterface
import psforever.net._
import scodec.Attempt.{Successful, Failure}
import scodec.bits._
import scodec.{Err, Attempt, Codec}
import scodec.codecs.{uint16L, uint8L, bytes}
import java.security.SecureRandom
/**
* Actor that stores crypto state for a connection and filters away any packet metadata.
*/
class CryptoSessionActor extends Actor with ActorLogging {
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
var cryptoDHState = new CryptoInterface.CryptoDHState()
var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None
val random = new SecureRandom()
// crypto handshake state
var serverChallenge = ByteVector.empty
var serverChallengeResult = ByteVector.empty
var serverMACBuffer = ByteVector.empty
var clientPublicKey = ByteVector.empty
var clientChallenge = ByteVector.empty
var clientChallengeResult = ByteVector.empty
def receive = Initializing
def Initializing : Receive = {
case HelloFriend(right) =>
leftRef = sender()
rightRef = right.asInstanceOf[ActorRef]
// who ever we send to has to send something back to us
rightRef ! HelloFriend(self)
context.become(NewClient)
case _ =>
log.error("Unknown message")
context.stop(self)
}
def NewClient : Receive = {
case RawPacket(msg) =>
// PacketCoding.DecodePacket
PacketCoding.UnmarshalPacket(msg) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case ControlPacket(_, ClientStart(nonce)) =>
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
context.become(CryptoExchange)
case default =>
log.error("Unexpected packet type " + p)
}
}
case default => log.error(s"Invalid message received ${default}")
}
def CryptoExchange : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) =>
// initialize our crypto state from the client's P and G
cryptoDHState.start(p, g)
// save the client challenge
clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge)
// save the packet we got for a MAC check later. drop the first 3 bytes
serverMACBuffer ++= msg.drop(3)
val serverTime = System.currentTimeMillis() / 1000L
val randomChallenge = getRandBytes(0xc)
// store the complete server challenge for later
serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge)
val packet = PacketCoding.CreateCryptoPacket(seq,
ServerChallengeXchg(serverTime, randomChallenge, cryptoDHState.getPublicKey))
val sentPacket = sendResponse(packet)
// save the sent packet a MAC check
serverMACBuffer ++= sentPacket.drop(3)
context.become(CryptoSetupFinishing)
case default => log.error("Unexpected packet type " + p)
}
}
case default => log.error(s"Invalid message received ${default}")
}
def CryptoSetupFinishing : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) =>
clientPublicKey = clientPubKey
clientChallengeResult = clientChalResult
// save the packet we got for a MAC check later
serverMACBuffer ++= msg.drop(3)
val agreedValue = cryptoDHState.agree(clientPublicKey)
/*println("Agreed: " + agreedValue)
println(s"Client challenge: $clientChallenge")*/
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
hex"00000000" ++ serverChallenge ++ hex"00000000"
//println("In message: " + agreedMessage)
val masterSecret = CryptoInterface.MD5MAC(agreedValue,
agreedMessage,
20)
//println("Master secret: " + masterSecret)
serverChallengeResult = CryptoInterface.MD5MAC(masterSecret,
ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01",
0xc)
val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret,
ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01",
0xc)
//println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult))
val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
/*println("DecExpansion: " + decExpansion)
println("EncExpansion: " + encExpansion)*/
// expand the encryption and decryption keys
// The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys
val expandedDecKey = CryptoInterface.MD5MAC(masterSecret,
decExpansion,
0x40) // this is what is visible in IDA
val expandedEncKey = CryptoInterface.MD5MAC(masterSecret,
encExpansion,
0x40)
val decKey = expandedDecKey.take(20)
val encKey = expandedEncKey.take(20)
val decMACKey = expandedDecKey.drop(20).take(16)
val encMACKey = expandedEncKey.drop(20).take(16)
/*println("**** DecKey: " + decKey)
println("**** EncKey: " + encKey)
println("**** DecMacKey: " + decMACKey)
println("**** EncMacKey: " + encMACKey)*/
// spin up our encryption program
cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey))
val packet = PacketCoding.CreateCryptoPacket(seq,
ServerFinished(serverChallengeResult))
sendResponse(packet)
context.become(Established)
case default => failWithError("Unexpected packet type " + default)
}
}
case default => failWithError(s"Invalid message received ${default}")
}
def Established : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg) match {
case Successful(p) =>
p match {
case encPacket @ EncryptedPacket(seq, _) =>
println("Decrypting packet..." + encPacket)
PacketCoding.decryptPacket(cryptoState.get, encPacket) match {
case Successful(packet) =>
println("RECV[E]: " + packet)
self ! packet
case Failure(e) =>
println("Failed to decode encrypted packet: " + e)
}
case default => failWithError("Unexpected packet type " + default)
}
case Failure(e) => println("Could not decode raw packet: " + e)
}
case ctrl @ ControlPacket(_, _) =>
val from = sender()
handleEstablishedPacket(from, ctrl)
case game @ GamePacket(_, _, _) =>
val from = sender()
handleEstablishedPacket(from, game)
/* case SlottedMetaPacket(innerPacket) =>
PacketCoding.DecodePacket(innerPacket) match {
case Successful(p) =>
println("RECV[INNER]: " + p)
val packet = PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(3,
LoginRespMessage("AAAABBBBCCCCDDDD",
hex"00000000 18FABE0C 00000000 00000000",
0, 1, 2, 685276011,
"AAAAAAAA", 0, false
))).require
sendResponse(packet)
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(
WorldInformation("PSForever", WorldStatus.Up, ServerType.Development,
Vector(WorldConnectionInfo(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 51001))), EmpireNeed.TR)
))
sendResponse(PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(4,
msg
)).require)
case Failure(e) => println("Failed to decode inner packet " + e)
}
*/
case default => failWithError(s"Invalid message received ${default}")
}
def failWithError(error : String) = {
log.error(error)
sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
}
def resetState() : Unit = {
context.become(receive)
// reset the crypto primitives
cryptoDHState.close
cryptoDHState = new CryptoInterface.CryptoDHState()
if(cryptoState.isDefined) {
cryptoState.get.close
cryptoState = None
}
serverChallenge = ByteVector.empty
serverChallengeResult = ByteVector.empty
serverMACBuffer = ByteVector.empty
clientPublicKey = ByteVector.empty
clientChallenge = ByteVector.empty
clientChallengeResult = ByteVector.empty
}
def handleEstablishedPacket(from : ActorRef, cont : PlanetSidePacketContainer) = {
// we are processing a packet we decrypted
if(from == self) {
rightRef ! cont
} else if(from == rightRef) { // processing a completed packet from the right. encrypt
val packet = PacketCoding.encryptPacket(cryptoState.get, cont).require
sendResponse(packet)
} else {
log.error("Invalid sender")
}
}
def sendResponse(cont : PlanetSidePacketContainer) : ByteVector = {
println("CRYPTO SEND: " + cont)
val pkt = PacketCoding.MarshalPacket(cont).require
val bytes = pkt.toByteVector
leftRef ! ResponsePacket(bytes)
bytes
}
def getRandBytes(amount : Int) : ByteVector = {
val array = Array.ofDim[Byte](amount)
random.nextBytes(array)
ByteVector.view(array)
}
}

View file

@ -1,12 +0,0 @@
// Copyright (c) 2016 PSForever.net to present
import java.net.InetSocketAddress
import akka.actor.ActorRef
import scodec.bits.{BitVector, ByteVector}
class LoginSession(id : Long, socket : ActorRef, address : InetSocketAddress) {
def send(msg : BitVector) = {
socket ! SendPacket(msg.toByteVector, address)
}
}

View file

@ -1,225 +1,65 @@
// Copyright (c) 2016 PSForever.net to present
import akka.actor.{Actor, ActorLogging}
import psforever.crypto.CryptoInterface.{CryptoStateWithMAC, CryptoState}
import psforever.crypto.CryptoInterface
import java.net.{InetAddress, InetSocketAddress}
import akka.actor.{ActorRef, Identify, Actor, ActorLogging}
import psforever.net._
import scodec.Attempt.{Successful, Failure}
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
import scodec.{Err, Attempt, Codec}
import scodec.codecs.{uint16L, uint8L, bytes}
import java.security.SecureRandom
/**
* Actor that stores crypto state for a connection and filters away any packet metadata.
* Also decrypts and handles packet retries using the sequence numbers.
* @param session Per session state
*/
class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging {
var cryptoDHState = new CryptoInterface.CryptoDHState()
var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None
val random = new SecureRandom()
class LoginSessionActor extends Actor with ActorLogging {
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
// crypto handshake state
var serverChallenge = ByteVector.empty
var serverChallengeResult = ByteVector.empty
var serverMACBuffer = ByteVector.empty
def receive = Initializing
var clientPublicKey = ByteVector.empty
var clientChallenge = ByteVector.empty
var clientChallengeResult = ByteVector.empty
def Initializing : Receive = {
case HelloFriend(right) =>
leftRef = sender()
rightRef = right.asInstanceOf[ActorRef]
def receive = NewClient
def NewClient : Receive = {
case RawPacket(msg) =>
// PacketCoding.DecodePacket
PacketCoding.UnmarshalPacket(msg) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case ControlPacket(_, ClientStart(nonce)) =>
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt()))))
context.become(CryptoExchange)
case default =>
log.error("Unexpected packet type " + p)
}
}
case default => log.error(s"Invalid message received ${default}")
context.become(Started)
case _ =>
log.error("Unknown message")
context.stop(self)
}
def CryptoExchange : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) =>
// initialize our crypto state from the client's P and G
cryptoDHState.start(p, g)
// save the client challenge
clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge)
// save the packet we got for a MAC check later. drop the first 3 bytes
serverMACBuffer ++= msg.drop(3)
val serverTime = System.currentTimeMillis() / 1000L
val randomChallenge = getRandBytes(0xc)
// store the complete server challenge for later
serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge)
val packet = PacketCoding.CreateCryptoPacket(seq,
ServerChallengeXchg(serverTime, randomChallenge, cryptoDHState.getPublicKey))
val sentPacket = sendResponse(packet)
// save the sent packet a MAC check
serverMACBuffer ++= sentPacket.drop(3)
context.become(CryptoSetupFinishing)
case default => log.error("Unexpected packet type " + p)
}
}
case default => log.error(s"Invalid message received ${default}")
def Started : Receive = {
case ctrl @ ControlPacket(opcode, pkt) =>
handleControlPkt(pkt)
case game @ GamePacket(opcode, seq, pkt) =>
handleGamePkt(pkt)
case default => failWithError(s"Invalid message received $default")
}
def CryptoSetupFinishing : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
case Failure(e) => log.error("Could not decode packet: " + e)
case Successful(p) =>
println("RECV: " + p)
p match {
case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) =>
clientPublicKey = clientPubKey
clientChallengeResult = clientChalResult
// save the packet we got for a MAC check later
serverMACBuffer ++= msg.drop(3)
val agreedValue = cryptoDHState.agree(clientPublicKey)
/*println("Agreed: " + agreedValue)
println(s"Client challenge: $clientChallenge")*/
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
hex"00000000" ++ serverChallenge ++ hex"00000000"
//println("In message: " + agreedMessage)
val masterSecret = CryptoInterface.MD5MAC(agreedValue,
agreedMessage,
20)
//println("Master secret: " + masterSecret)
serverChallengeResult = CryptoInterface.MD5MAC(masterSecret,
ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01",
0xc)
val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret,
ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01",
0xc)
//println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult))
val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
hex"00000000" ++ clientChallenge ++ hex"00000000"
/*println("DecExpansion: " + decExpansion)
println("EncExpansion: " + encExpansion)*/
// expand the encryption and decryption keys
// The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys
val expandedDecKey = CryptoInterface.MD5MAC(masterSecret,
decExpansion,
0x40) // this is what is visible in IDA
val expandedEncKey = CryptoInterface.MD5MAC(masterSecret,
encExpansion,
0x40)
val decKey = expandedDecKey.take(20)
val encKey = expandedEncKey.take(20)
val decMACKey = expandedDecKey.drop(20).take(16)
val encMACKey = expandedEncKey.drop(20).take(16)
/*println("**** DecKey: " + decKey)
println("**** EncKey: " + encKey)
println("**** DecMacKey: " + decMACKey)
println("**** EncMacKey: " + encMACKey)*/
// spin up our encryption program
cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey))
val packet = PacketCoding.CreateCryptoPacket(seq,
ServerFinished(serverChallengeResult))
sendResponse(packet)
context.become(Established)
case default => failWithError("Unexpected packet type " + default)
}
}
case default => failWithError(s"Invalid message received ${default}")
}
def Established : Receive = {
case RawPacket(msg) =>
PacketCoding.UnmarshalPacket(msg) match {
case Successful(p) =>
p match {
case encPacket @ EncryptedPacket(seq, _) =>
println("Decrypting packet..." + encPacket)
PacketCoding.decryptPacket(cryptoState.get, encPacket) match {
case Successful(packet) =>
println("RECV[E]: " + packet)
self ! packet
case Failure(e) =>
println("Failed to decode encrypted packet: " + e)
}
case default => failWithError("Unexpected packet type " + default)
}
case Failure(e) => println("Could not decode raw packet: " + e)
}
case ctrl @ ControlPacket(_, pkt) => pkt match {
def handleControlPkt(pkt : PlanetSideControlPacket) = {
pkt match {
case SlottedMetaPacket(innerPacket) =>
PacketCoding.DecodePacket(innerPacket) match {
case Successful(p) =>
println("RECV[INNER]: " + p)
val packet = PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(3,
LoginRespMessage("AAAABBBBCCCCDDDD",
hex"00000000 18FABE0C 00000000 00000000",
0, 1, 2, 685276011,
"AAAAAAAA", 0, false
))).require
val packet = LoginRespMessage("AAAABBBBCCCCDDDD",
hex"00000000 18FABE0C 00000000 00000000",
0, 1, 2, 685276011,
"AAAAAAAA", 0, false
)
sendResponse(packet)
sendResponse(PacketCoding.CreateGamePacket(0, packet))
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(
WorldInformation("gemini", WorldStatus.Up, ServerType.Released, Vector(), EmpireNeed.NC)
WorldInformation("PSForever", WorldStatus.Up, ServerType.Development,
Vector(WorldConnectionInfo(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 51001))), EmpireNeed.TR)
))
sendResponse(PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(4,
msg
)).require)
sendResponse(PacketCoding.CreateGamePacket(0, msg))
case Failure(e) => println("Failed to decode inner packet " + e)
}
}
case default => failWithError(s"Invalid message received ${default}")
}
def handleGamePkt(pkt : PlanetSideGamePacket) = {
}
def failWithError(error : String) = {
@ -227,36 +67,8 @@ class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging
sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
}
def resetState() : Unit = {
context.become(receive)
// reset the crypto primitives
cryptoDHState.close
cryptoDHState = new CryptoInterface.CryptoDHState()
if(cryptoState.isDefined) {
cryptoState.get.close
cryptoState = None
}
serverChallenge = ByteVector.empty
serverChallengeResult = ByteVector.empty
serverMACBuffer = ByteVector.empty
clientPublicKey = ByteVector.empty
clientChallenge = ByteVector.empty
clientChallengeResult = ByteVector.empty
}
def sendResponse(cont : PlanetSidePacketContainer) : ByteVector = {
println("SEND: " + cont)
val pkt = PacketCoding.MarshalPacket(cont).require
session.send(pkt)
pkt.toByteVector
}
def getRandBytes(amount : Int) : ByteVector = {
val array = Array.ofDim[Byte](amount)
random.nextBytes(array)
ByteVector.view(array)
def sendResponse(cont : PlanetSidePacketContainer) = {
log.info("LOGIN SEND: " + cont)
rightRef ! cont
}
}

View file

@ -1,36 +1,88 @@
// Copyright (c) 2016 PSForever.net to present
import java.net.InetSocketAddress
import akka.actor.{Props, ActorRef, ActorLogging, Actor}
import akka.actor._
import scodec.bits._
import scala.collection.mutable
final case class RawPacket(data : ByteVector)
final case class ResponsePacket(data : ByteVector)
case class SessionState(id : Long, address : InetSocketAddress, pipeline : List[ActorRef]) {
def inject(pkt : RawPacket) = pipeline.head ! pkt
}
class SessionRouter extends Actor with ActorLogging {
val sessions = mutable.Map[InetSocketAddress, ActorRef]()
var sessionId = 0L
val idBySocket = mutable.Map[InetSocketAddress, Long]()
val sessionById = mutable.Map[Long, SessionState]()
val sessionByActor = mutable.Map[ActorRef, SessionState]()
def receive = {
var sessionId = 0L // this is a connection session, not an actual logged in session ID
var inputRef : ActorRef = ActorRef.noSender
/*
Login sessions are divided between two actors. the crypto session actor transparently handles all of the cryptographic
setup of the connection. Once a correct crypto session has been established, all packets, after being decrypted
will be passed on to the login session actor. This actor has important state that is used to maintain the login
session.
> PlanetSide Session Pipeline <
read() route decrypt
UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
^ | ^ | ^ |
| write() | | encrypt | | response |
+--------------+ +-----------+ +-----------------+
*/
def receive = initializing
def initializing : Receive = {
case Hello() =>
inputRef = sender()
context.become(started)
case _ =>
log.error("Unknown message")
context.stop(self)
}
def started : Receive = {
case ReceivedPacket(msg, from) =>
if(sessions.contains(from)) {
sessions{from} ! RawPacket(msg)
if(idBySocket.contains(from)) {
sessionById{idBySocket{from}}.inject(RawPacket(msg))
} else {
log.info("New session from " + from.toString)
val id = newSessionId
val loginSession = new LoginSession(id, sender(), from)
val ref = context.actorOf(Props(new LoginSessionActor(loginSession)),
"login-session" + id.toString)
val session = createNewSession(from)
idBySocket{from} = session.id
sessions{from} = ref
sessionById{session.id} = session
sessionByActor{session.pipeline.head} = session
ref ! RawPacket(msg)
sessionById{session.id}.inject(RawPacket(msg))
}
case ResponsePacket(msg) =>
val session = sessionByActor{sender()}
inputRef ! SendPacket(msg, session.address)
case _ => log.error("Unknown message")
}
def createNewSession(address : InetSocketAddress) = {
val id = newSessionId
val cryptoSession = context.actorOf(Props[CryptoSessionActor],
"crypto-session" + id.toString)
val loginSession = context.actorOf(Props[LoginSessionActor],
"login-session" + id.toString)
// start the pipeline setup
cryptoSession ! HelloFriend(loginSession)
SessionState(id, address, List(cryptoSession, loginSession))
}
def newSessionId = {
val oldId = sessionId
sessionId += 1

View file

@ -1,13 +1,15 @@
// Copyright (c) 2016 PSForever.net to present
import java.net.InetSocketAddress
import akka.actor.{ActorLogging, Actor, ActorRef}
import akka.actor.{Identify, ActorLogging, Actor, ActorRef}
import akka.io._
import scodec.bits.ByteVector
import scodec.interop.akka._
final case class ReceivedPacket(msg : ByteVector, from : InetSocketAddress)
final case class SendPacket(msg : ByteVector, to : InetSocketAddress)
final case class Hello()
final case class HelloFriend(next: ActorRef)
class UdpListener(nextActor: ActorRef) extends Actor with ActorLogging {
import context.system
@ -19,6 +21,8 @@ class UdpListener(nextActor: ActorRef) extends Actor with ActorLogging {
def receive = {
case Udp.Bound(local) =>
println("UDP bound: " + local)
nextActor ! Hello()
context.become(ready(sender()))
}