Skip to content
Mats Vederhus edited this page Aug 27, 2023 · 17 revisions

Welcome to the Parlo wiki!

Here's an example of how to use Parlo to create a safe and secure communication protocol. The following example uses SRP.NET and ZeroFormatter for this purpose. They are available as nuGet packages.

using ZeroFormatter;

namespace ParloProtocol
{
    public enum AuthPacketIDs
    {
        ClientSignup = 0x00,
        ClientInitialAuth = 0x01,
        ServerInitialAuthResponse = 0x02,
        CAuthProof = 0x03, //Sent by client
        SAuthProof = 0x04 //Sent by server
    }

    /// <summary>
    /// The packet sent by the client to the server (may or may not be a different server from the login server)
    /// to sign up (I.E create an account in the DB).
    /// </summary>
    [ZeroFormattable]
    public struct ClientSignup : IPacket
    {
        public ClientSignup(string userName, string salt, string verifier)
        {
            Username = userName;
            Salt = salt;
            Verifier = verifier;
        }

        [Index(0)]
        public string Username = default!;

        [Index(1)]
        public string Salt = default!;

        [Index(2)]
        public string Verifier = default!;
    }

    /// <summary>
    /// The packet sent by the client to the server to initiate authentication.
    /// </summary>
    [ZeroFormattable]
    public struct ClientInitialAuth : IPacket
    {
        public ClientInitialAuth(string userName, string ephemeral)
        {
            Username = userName;
            Ephemeral = ephemeral;
        }

        [Index(0)]
        public string Username = default!;

        [Index(1)]
        public string Ephemeral = default!;
    }

    /// <summary>
    /// The initial response from the server.
    /// </summary>
    [ZeroFormattable]
    public struct ServerInitialAuthResponse : IPacket
    {
        public ServerInitialAuthResponse(string salt, string publicEphemeral)
        {
            Salt = salt;
            PublicEphemeral = publicEphemeral;
        }

        [Index(0)]
        public string Salt = default!;

        [Index(1)]
        public string PublicEphemeral = default!;
    }

    /// <summary>
    /// Sent by the client to the server and vice versa.
    /// </summary>
    [ZeroFormattable]
    public struct AuthProof : IPacket
    {
        public AuthProof(string sessionProof)
        {
            SessionProof = sessionProof;
        }

        [Index(0)]
        public string SessionProof = default!;
    }
} 

ClientNetworkManager:

using Parlo;
using Parlo.Packets;
using SecureRemotePassword;
using ZeroFormatter;

namespace ParloProtocol
{
    public delegate Task OnPacketReceivedDelegate(IPacket Packet, byte ID, NetworkClient Sender);

    public class ClientNetworkManager
    {
        private static Lazy<ClientNetworkManager> m_Instance = new Lazy<ClientNetworkManager>(() => new ClientNetworkManager());
        private static NetworkClient m_Client = default!;

        private static SrpClient m_SRPClient = new SrpClient();
        private static SrpEphemeral m_ClientEphemeral = default!;

        private static Task m_NetworkingTask = default!;

        private static bool m_HasBeenAuthenticated = false;

        /// <summary>
        /// Has the client been authenticated?
        /// This means it should start sending 
        /// encrypted packets to the server.
        /// </summary>
        public static bool HasBeenAuthenticated { get { return m_HasBeenAuthenticated; } }

        /// <summary>
        /// Event invoked when the client has connected.
        /// </summary>
        public static event OnConnectedDelegate OnConnected = default!;

        /// <summary>
        /// Event invoked when a network error occurred. 
        /// </summary>
        public static event NetworkErrorDelegate OnNetworkError = default!;

        /// <summary>
        /// Event invoked when the client received a packet.
        /// </summary>
        public static event OnPacketReceivedDelegate OnPacketReceived = default!;

        /// <summary>
        /// Gets an instance of this ClientNetworkManager.
        /// </summary>
        public static ClientNetworkManager Instance { get { return m_Instance.Value; } }

        /// <summary>
        /// Connects to a remote server.
        /// </summary>
        /// <param name="IP">The IP to connect to.</param>
        /// <param name="Port">The port to use.</param>
        /// <param name="Username">The player's username.</param>
        /// <param name="Password">The player's password.</param>
        public void Connect(string IP, int Port, string Username, string Password)
        {
            m_Client = new NetworkClient(new ParloSocket(true));
            m_Client.OnConnected += Client_OnConnected;
            m_Client.OnNetworkError += Client_OnNetworkError;
            m_Client.OnReceivedData += Client_OnReceivedData;

            LoginArgsContainer LoginArgs = new LoginArgsContainer();
            LoginArgs.Address = IP;
            LoginArgs.Port = Port;
            LoginArgs.Client = m_Client;
            LoginArgs.Username = Username;
            LoginArgs.Password = Password;

            if (PacketHandlers.Get((byte)AuthPacketIDs.ServerInitialAuthResponse) == null)
            {
                PacketHandlers.Register((byte)AuthPacketIDs.ServerInitialAuthResponse, false,
                    new OnPacketReceived(Client_OnReceivedData));
            }

            m_NetworkingTask = Task.Run(async () => { await m_Client.ConnectAsync(LoginArgs); });
        }

        /// <summary>
        /// Sends a packet to the server.
        /// </summary>
        /// <param name="Packet">The packet to send.</param>
        public Task SendAsync(Packet P)
        {
            return m_Client.SendAsync(P.BuildPacket());
        }

        /// <summary>
        /// Sends a encrypted packet to the server.
        /// </summary>
        /// <param name="Packet">The encrypted packet to send.</param>
        public Task SendAsync(EncryptedPacket P)
        {
            if (!m_HasBeenAuthenticated)
                throw new InvalidOperationException("Cannot send encrypted packets before authentication.");

            return m_Client.SendAsync(P.BuildPacket());
        }

        /// <summary>
        /// The client connected to the given server.
        /// This starts the SRP6 authentication process by sending an initial packet.
        /// </summary>
        /// <param name="LoginArgs">The LoginArgsContainer created based on the args passed to Connect().</param>
        private async Task Client_OnConnected(LoginArgsContainer LoginArgs)
        {
            await OnConnected?.Invoke(LoginArgs);

            ClientInitialAuth InitialAuth = new ClientInitialAuth();
            InitialAuth.Username = LoginArgs.Username;

            string Salt = m_SRPClient.GenerateSalt();
            string PrivateKey = m_SRPClient.DerivePrivateKey(Salt, LoginArgs.Username, LoginArgs.Password);
            string Verifier = m_SRPClient.DeriveVerifier(PrivateKey);

            m_ClientEphemeral = m_SRPClient.GenerateEphemeral();
            InitialAuth.Ephemeral = m_ClientEphemeral.Public;
            byte[] Data = ZeroFormatterSerializer.Serialize(InitialAuth);
        }

        /// <summary>
        /// A network error occurred!
        /// </summary>
        /// <param name="Exception">The SocketException that was thrown.</param>
        private void Client_OnNetworkError(System.Net.Sockets.SocketException Exception)
        {
            OnNetworkError?.Invoke(Exception);
        }

        /// <summary>
        /// The client received a packet!
        /// </summary>
        /// <param name="Packet">The packet that the client received.</param>
        private async Task Client_OnReceivedData(NetworkClient Sender, Packet P)
        {
            switch (P.ID)
            {
                case (byte)AuthPacketIDs.ServerInitialAuthResponse:
                    ServerInitialAuthResponse InitialAuthResponsePacket =
                        ZeroFormatterSerializer.Deserialize<ServerInitialAuthResponse>(P.Data);
                    await OnPacketReceived?.Invoke(InitialAuthResponsePacket, P.ID, Sender);
                    break;
                case (byte)AuthPacketIDs.SAuthProof:
                    m_HasBeenAuthenticated = true;
                    AuthProof AuthProofPacket = ZeroFormatterSerializer.Deserialize<AuthProof>(P.Data);
                    await OnPacketReceived?.Invoke(AuthProofPacket, P.ID, Sender);
                    break;
            }
        }
    }
}

ServerNetworkManager:

using Parlo;
using Parlo.Packets;
using SecureRemotePassword;
using System.Collections.Concurrent;
using System.Net;
using ZeroFormatter;

namespace ParloProtocol
{
    public class ServerNetworkManager
    {
        private static readonly Lazy<ServerNetworkManager> m_Instance = new(() => new ServerNetworkManager());
        private Listener m_Listener = default!;

        private SrpServer m_SRPServer = new SrpServer();
        private SrpEphemeral m_ServerEphemeral = new SrpEphemeral();

        private Task m_NetworkingTask = default!;

        private ConcurrentDictionary<Guid, NetworkClient> m_Clients = new ConcurrentDictionary<Guid, NetworkClient>();

        /// <summary>
        /// Event invoked when a network error occurred. 
        /// </summary>
        public event NetworkErrorDelegate OnNetworkError = default!;

        /// <summary>
        /// Gets an instance of this ServerNetworkManager.
        /// </summary>
        public static ServerNetworkManager Instance { get { return m_Instance.Value; } }

        /// <summary>
        /// Event invoked when a client received a packet.
        /// </summary>
        public event OnPacketReceivedDelegate OnPacketReceived = default!;

        public async Task Listen(string IP, int Port)
        {
            await using Listener m_Listener = new Listener(new ParloSocket(false));
            m_Listener.OnConnected += M_Listener_OnConnected;
            m_Listener.OnDisconnected += M_Listener_OnDisconnected;

            IPEndPoint Endpoint = new IPEndPoint(IPAddress.Parse(IP), Port);
            PacketHandlers.Register((byte)AuthPacketIDs.ClientInitialAuth, false, 
                new OnPacketReceived(Client_OnReceivedData));
            PacketHandlers.Register((byte)AuthPacketIDs.CAuthProof, false, 
                new OnPacketReceived(Client_OnReceivedData));

            await m_Listener.InitializeAsync(Endpoint);
        }

        /// <summary>
        /// A new client connected!
        /// </summary>
        /// <param name="Client">The client that connected.</param>
        private async Task M_Listener_OnConnected(NetworkClient Client)
        {
            await Task.Run(() =>
            {
                Guid NewClientGuid = Guid.NewGuid();

                //False means the key already existed which should be impossible.
                if (!m_Clients.TryAdd(NewClientGuid, Client))
                    throw new NetworkException("ServerNetworkManager: Key already existed!");

                Client.OnReceivedData += Client_OnReceivedData;
            });
        }

        /// <summary>
        /// Received a packet from a client.
        /// </summary>
        /// <param name="Packet">The packet that was received.</param>
        private async Task Client_OnReceivedData(NetworkClient Sender, Packet P)
        {
            //PacketStream PStream = (PacketStream)Packet;

            switch (P.ID)
            {
                case (byte)AuthPacketIDs.ClientSignup:
                    ClientSignup SignUpPacket = ZeroFormatterSerializer.Deserialize<ClientSignup>(P.Data);
                    await OnPacketReceived?.Invoke(SignUpPacket, P.ID, Sender);
                    break;
                case (byte)AuthPacketIDs.ClientInitialAuth:
                    ClientInitialAuth InitialAuthPacket = ZeroFormatterSerializer.Deserialize<ClientInitialAuth>(P.Data);
                    await OnPacketReceived?.Invoke(InitialAuthPacket, P.ID, Sender);
                    break;
                case (byte)AuthPacketIDs.CAuthProof:
                    AuthProof AuthProofPacket = ZeroFormatterSerializer.Deserialize<AuthProof>(P.Data);
                    await OnPacketReceived?.Invoke(AuthProofPacket, P.ID, Sender);
                    break;
            }
        }

        /// <summary>
        /// A client disconnected!
        /// </summary>
        /// <param name="Client">The client that disconnected.</param>
        private async Task M_Listener_OnDisconnected(NetworkClient Client)
        {
        }
    }
}

NetworkException:


namespace ParloProtocol
{
    /// <summary>
    /// Thrown when something network-related goes awry!
    /// </summary>
    internal class NetworkException : Exception
    {
        /// <summary>
        /// Creates a NetworkException.
        /// </summary>
        /// <param name="Message">The message that goes with the exception.</param>
        public NetworkException(string Message) : base(Message)
        {

        }
    }
}

Clone this wiki locally