IT knowledge base
CTRL+F per cercare la tua parola chiave

Implementazione di un sistema di comunicazione sicuro asincrono basato su socket TCP e un server OpenVPN centrale

In questo articolo voglio parlarvi della mia implementazione di un messenger con messaggi doppiamente criptati basati su socket tcp, server OpenVPN, server PowerDNS.
L'essenza del compito è garantire lo scambio di messaggi su Internet bypassando NAT tra client su varie piattaforme (IOS, Android, Windows, Linux). È inoltre necessario garantire la sicurezza dei dati trasmessi.
Il mio sistema è composto da:
  • Server OpenVPN
  • Server PowerDNS
  • Procura. Nel mio caso si tratta di un servizio Web scritto in Ruby on Rails con l'implementazione del protocollo SOAP. Cioè, su questo server SOAP, descrivo il meccanismo per eseguire determinate azioni, quindi eseguo una richiesta SOAP dal terminale client, il server esegue determinate azioni, ad esempio aggiornando la zona PowerDNS e restituisce una risposta al terminale - con successo o senza successo. Molto comodamente.
  • terminali direttamente client.
Sui terminali client, viene generato un socket tcp che ascolta su una porta specifica i messaggi in arrivo. Se il messaggio arriva, il socket lo invia al terminale. Il messaggio stesso contiene il nome utente del mittente. Viene aperto anche il socket tcp del client per inviare messaggi.
Il socket viene aperto direttamente con il terminale del client remoto. Pertanto, nel mio caso, l'interazione è in modalità client-client.
I clienti vengono cercati per nome utente. L'associazione del nome utente e dell'indirizzo IP viene archiviata dal server PowerDNS.
PowerDNS è un server DNS ad alte prestazioni scritto in C++ e con licenza GPL. Lo sviluppo viene effettuato nell'ambito del supporto per i sistemi Unix; I sistemi Windows non sono più supportati.
Il server è sviluppato dalla società olandese PowerDNS.com di Bert Hubert ed è supportato dalla comunità del software libero.
PowerDNS utilizza un'architettura flessibile di archiviazione/accesso ai dati in grado di recuperare le informazioni DNS da qualsiasi origine dati. Ciò include file, file di zona BIND, database relazionali o directory LDAP.
PowerDNS è configurato per impostazione predefinita per soddisfare le richieste dal database.
Dopo il rilascio della versione 2.9.20, il software viene distribuito sotto forma di due componenti: Server (Authoritative) (DNS autorevole) e Recursor (DNS ricorsivo).
La scelta di questa soluzione è dovuta al fatto che PowerDNS è in grado di lavorare con un database senza collegare moduli aggiuntivi, oltre ad una velocità maggiore rispetto ad altre soluzioni gratuite.
Per i miei scopi, mi è bastato solo il modulo autorevole, non ho installato il ricorsore.
I terminali client comunicano con tutti i componenti interni tramite il gateway SOAP.
La logica del lavoro è la seguente: il client accende il programma, viene eseguito il metodo SOAP per aggiornare la zona sul server PowerDNS. Se il client vuole contattare qualcuno, inserisce o seleziona il nome utente corrispondente dall'elenco, viene eseguito il metodo SOAP per ottenere un indirizzo IP dal database DNS e viene stabilita una connessione al client remoto tramite l'indirizzo IP.
Ho scritto client già pronti per IOS, Android, Windows. Quando li ho scritti, ho usato il xamarin. È molto conveniente, sono bastate solo piccole modifiche al codice per trasferire l'applicazione su un'altra piattaforma.
Successivamente, presenterò i codici socket client e server che utilizzo sui terminali client. Ecco i codici per IOS. Per Android e Windows sarà quasi così. L'unica differenza è nei diversi tipi di elementi (pulsanti, blocchi di testo, ecc.)
Codice socket tcp del server
    public class GlobalFunction
    {

        public static void writeLOG (string loggg)
        {
            // Reflecting on the past can serve as a guide for the future
            string path = @ "bin \ logfile.log";
            string time = DateTime.Now.ToString ("hh: mm: ss");
            string date = DateTime.Now.ToString ("yyyy.MM.dd");
            string logging = date + "" + time + "" + loggg;

            using (StreamWriter sw = File.AppendText (path))
                {
                    sw.WriteLine (logging);
                }
        }

        public static void writeLOGdebug (string loggg)
        {
            try
            {
                // Reflecting on the past can guide the future
                string path = @ "bin \ logfile.log";
                string time = DateTime.Now.ToString ("hh: mm: ss");
                string date = DateTime.Now.ToString ("yyyy.MM.dd");
                string logging = date + "" + time + "" + loggg;

                using (StreamWriter sw = File.AppendText (path))
                {
                    sw.WriteLine (logging);
                }
            }
            catch (Exception exc) {}
        }
    }

    public class Globals
    {
        public static IPAddress localip = "192.168.88.23";
        public static int _localServerPort = 19991;
        public const int _maxMessage = 100;
        public static string _LocalUserName = "375297770001";

        public struct MessBuffer
        {
            public string usernameLocal;
            public string usernamePeer;
            public string message;
        }


        public static List <MessBuffer> MessagesBase = new List <MessBuffer> ();
        public static List <MessBuffer> MessagesBaseSelected = new List <MessBuffer> ();

    }

    public class StateObject
    {
        // Client socket.
        public Socket workSocket = null;
        // Size of receive buffer.
        public const int BufferSize = 1024;
        // Receive buffer.
        public byte [] buffer = new byte [BufferSize];
        // Received data string.
        public StringBuilder sb = new StringBuilder ();
    }

    public partial class ViewController: UIViewController
    {

        public static ManualResetEvent allDone = new ManualResetEvent (false);
        public void startLocalServer ()
        {
            // IPHostEntry ipHost = Dns.GetHostEntry (_serverHost);
            // IPAddress ipAddress = ipHost.AddressList [0];
            IPAddress ipAddress = Globals.localip;
            IPEndPoint ipEndPoint = new IPEndPoint (ipAddress, Globals._localServerPort);
            Socket socket = new Socket (ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            socket.Bind (ipEndPoint);
            socket.Listen (1000);
            GlobalFunction.writeLOGdebug ("Local Server has been started on IP:" + ipEndPoint);
            while (true)
            {
                try
                {
                    // Set the event to nonsignaled state.
                    allDone.Reset ();

                    // Start an asynchronous socket to listen for connections.
                    socket.BeginAccept (
                        new AsyncCallback (AcceptCallback),
                        socket);

                    // Wait until a connection is made before continuing.
                    allDone.WaitOne ();
                }
                catch (Exception exp) {GlobalFunction.writeLOGdebug ("Error. Failed startLocalServer () method:" + Convert.ToString (exp)); }
            }

        }

        public void AcceptCallback (IAsyncResult ar)
        {
            // Signal the main thread to continue.
            allDone.Set ();

            // Get the socket that handles the client request.
            Socket listener = (Socket) ar.AsyncState;
            Socket handler = listener.EndAccept (ar);

            // Create the state object.
            StateObject state = new StateObject ();
            state.workSocket = handler;
            handler.BeginReceive (state.buffer, 0, StateObject.BufferSize, 0,
                new AsyncCallback (ReadCallback), state);
        }

        public void ReadCallback (IAsyncResult ar)
        {
            String content = String.Empty;

            // Retrieve the state object and the handler socket
            // from the asynchronous state object.
            StateObject state = (StateObject) ar.AsyncState;
            Socket handler = state.workSocket;

            // Read data from the client socket.
            int bytesRead = handler.EndReceive (ar);

            if (bytesRead> 0)
            {
                // There might be more data, so store the data received so far.
                state.sb.Append (Encoding.UTF8.GetString (
                    state.buffer, 0, bytesRead));

                // Check for end-of-file tag. If it is not there, read
                // more data.
                content = state.sb.ToString ();
                if (content.IndexOf ("<EOF>")> -1)
                {
                    // All the data has been read from the
                    // client. Display it on the console.

                    string [] bfd = content.Split (new char [] {'|'}, StringSplitOptions.None);
                    string decrypt = MasterEncryption.MasterDecrypt (bfd [0]);

                    string [] bab = decrypt.Split (new char [] {'~'}, StringSplitOptions.None);
                    Globals.MessBuffer Bf = new Globals.MessBuffer ();
                    Bf.message = bab [2];
                    Bf.usernamePeer = bab [0];
                    Bf.usernameLocal = bab [1];
                    string upchat_m = "[" + bab [1] + "] #" + bab [2];



                    this.InvokeOnMainThread (delegate {
                        frm.messageField1.InsertText (Environment.NewLine + "[" + bab [1] + "] #" + bab [2]);
                          
                    });

                    // if (Ok! = null) {Ok (this, upchat_m); }
                    Globals.MessagesBase.Add (Bf);
                    //GlobalFunction.writeLOGdebug("Received message: "+ content);

                    // Echo the data back to the client.
                    // Send (handler, content);
                }
                else
                {
                    handler.BeginReceive (state.buffer, 0, StateObject.BufferSize, 0,
                    new AsyncCallback (ReadCallback), state);
                }
            }
        }
}
Il server si avvia in un thread separato, ad esempio in questo modo:
    public static class Program
    {
        private static Thread _serverLocalThread;
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        /// 
        [STAThread]
        static void Main()
        {
            _serverLocalThread = new Thread(GlobalFunction.startLocalServer);
            _serverLocalThread.IsBackground = true;
            _serverLocalThread.Start();

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
Codice client socket TCP
        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }

        private void connectToRemotePeer(IPAddress ipAddress)
        {
            try
            {
                IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, Globals._localServerPort);
                _serverSocketClientRemote = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                _serverSocketClientRemote.Connect(ipEndPoint);

                GlobalFunction.writeLOGdebug("We connected to:  " + ipEndPoint);
            }
            catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed connectToRemotePeer(string iphost) method:  " + Convert.ToString(exc)); }
        }

        private void sendDataToPeer(string textMessage)
        {
            try
            {
                byte[] buffer = Encoding.UTF8.GetBytes(textMessage);
                int bytesSent = _serverSocketClientRemote.Send(buffer);

                GlobalFunction.writeLOGdebug("Sended data: " + textMessage);
            }
            catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed sendDataToPeer(string testMessage) method:  " + Convert.ToString(exc)); }
        }

        private void Client_listner()
        {
            try
            {
                while (_serverSocketClientRemote.Connected)
                {
                    byte[] buffer = new byte[8196];
                    int bytesRec = _serverSocketClientRemote.Receive(buffer);
                    string data = Encoding.UTF8.GetString(buffer, 0, bytesRec);
                    //string data1 = encryption.Decrypt(data);
                    if (data.Contains("#updatechat"))
                    {
                        //UpdateChat(data);
                        GlobalFunction.writeLOGdebug("Chat updated with: " + data);

                        continue;
                    }
                }
            }
            catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed Client_listner() method:  " + Convert.ToString(exc)); }
        }

        private void sendMessage()
        {
            try
            {
                connectToRemotePeer(Globals._peerRemoteServer);
                _RemoteclientThread = new Thread(Client_listner);
                _RemoteclientThread.IsBackground = true;
                _RemoteclientThread.Start();

                string data = inputTextBox.Text;

                Globals.MessBuffer ba = new Globals.MessBuffer();
                ba.usernameLocal = Globals._LocalUserName;
                ba.usernamePeer = Globals._peerRemoteUsername;
                ba.message = data;
                Globals.MessagesBase.Add(ba);


                if (string.IsNullOrEmpty(data)) return;
                string datatopeer = Globals._peerRemoteUsername + "~" + Globals._LocalUserName + "~" + data;
                string datatopeerEncrypted = MasterEncryption.MasterEncrypt(datatopeer);
                sendDataToPeer(datatopeerEncrypted + "|<EOF>");
                addLineToChat(data, Globals._LocalUserName);
                inputTextBox.Text = string.Empty;
            }
            catch (Exception exp) { GlobalFunction.writeLOGdebug(Convert.ToString(exp)); }
        }

        private void addLineToChat(string msg, string username)
        {
            messageField1.InsertText(Environment.NewLine + "[" + username + "]# " + msg);
        }

        public void addFromServer(string msg, string username)
        {
            messageField1.InsertText(Environment.NewLine + "[" + username + "]# " + msg);
        }

        private void listBox1_Click()
        {
            messageField1.Text = "";
            Globals.MessagesBaseSelected.Clear();
            GlobalFunction.ReloadLocalBufferForSelected();

            for (int i = 0; i < Globals.MessagesBaseSelected.Count; i++)
            {
                messageField1.InsertText(Environment.NewLine + "[" + Globals.MessagesBaseSelected[i].usernameLocal + "]# " + Globals.MessagesBaseSelected[i].message);
            }

            Globals._peerRemoteServer = GlobalFunction.getPEERIPbySOAPRequest(Globals._peerRemoteUsername);
             
            string Name = Globals._LocalUserName;
            GlobalFunction.writeLOGdebug("Local name parameter listBox1_DoubleClick: " + Name);
            connectToRemotePeer(Globals._peerRemoteServer);
            _RemoteclientThread = new Thread(Client_listner);
            _RemoteclientThread.IsBackground = true;
            _RemoteclientThread.Start();
        }
        public static ViewController Form;

È necessario un server OpenVPN per attraversare NAT e per una protezione aggiuntiva dei dati.
Nel caso di OpenVPN, abbiamo una sorta di indirizzamento all'interno del tunnel attraverso il quale i client possono comunicare.
Non descriverò il processo di installazione del server OpenVPN, poiché ci sono molte informazioni su questo argomento. Darò solo la mia configurazione. È completamente ottimizzato per i miei scopi e completamente funzionante (copiato dal server funzionante così com'è, senza modifiche, ha solo cambiato l'IP nella configurazione del client in falso, invece di 1.1.1.1 è necessario specificare l'indirizzo IP del tuo server OpenVPN ).
OpenVPN esegue tutte le operazioni di rete tramite il trasporto TCP o UDP. In generale, l'UDP è preferibile per il motivo che il livello di rete e il traffico superiore passano attraverso il tunnel tramite OSI se viene utilizzata una connessione TUN, o il traffico di livello di collegamento e superiore se si utilizza TAP. Ciò significa che OpenVPN per il client funge da canale o anche protocollo di livello fisico, il che significa che l'affidabilità della trasmissione dei dati può essere fornita dai livelli OSI superiori, se necessario. Ecco perché il protocollo UDP è il concetto più vicino a OpenVPN. esso, come i protocolli di collegamento dati e di livello fisico, non garantisce l'affidabilità della connessione, passando questa iniziativa a livelli superiori. Se configuri il tunnel per funzionare su TCP, il server riceverà in genere segmenti TCP OpenVPN che contengono altri segmenti TCP dal client. Di conseguenza, si ottiene un doppio controllo per l'integrità delle informazioni nella catena, che non ha alcun senso, poiché l'affidabilità non è migliorata e le velocità di connessione e ping sono ridotte.
Configurazione del mio server OpenVPN
porta 8443
proto udp
dev tun
cd / etc / openvpn
tasto di persistenza
persist-tun
tls-server
tls-timeout 120
#certificati e crittografia
dh /etc/openvpn/keys/dh2048.pem
ca /etc/openvpn/keys/ca.crt
cert /etc/openvpn/keys/server.crt
chiave /etc/openvpn/keys/server.key
tls-auth /etc/openvpn/keys/ta.key 0
auth SHA512
cifrario AES-256-CBC
duplicato-cn
da cliente a cliente
#Informazioni DHCP
server 10.0.141.0 255.255.255.0
sottorete della topologia
max-clienti 250
percorso 10.0.141.0 255.255.255.0
#qualunque
mssfix
galleggiante
comp-lzo
muto 20
#log e sicurezza
utente nessuno
gruppo nessun gruppo
keepalive 10 120
stato /var/log/openvpn/openvpn-status.log
log-append /var/log/openvpn/openvpn.log
client-config-dir / etc / openvpn / ccd
verbo 3
Configurazione del mio client OpenVPN
cliente
dev tun
proto udp
remoto 1.1.1.1 8443
tls-client
#ca ca.crt
#cert client1.crt
#chiave cliente1.chiave
chiave-direzione 1
# tls-auth ta.key 1
auth SHA512
cifrario AES-256-CBC
server-cert-tls remoto
comp-lzo
tun-mtu 1500
mssfix
ping-riavvia 180
tasto di persistenza
persist-tun
auth-nocache
verbo 3
I commenti su questa configurazione OpenVPN attendono.
Tutte le richieste dai terminali client vengono effettuate all'indirizzo del tunnel sul lato del server OpenVPN, dove sono configurati l'inoltro delle porte appropriato e l'accesso necessario.
Ecco un esempio:
iptables -A INPUT -i ens160 -p tcp -m tcp --dport 8443 -j ACCEPT
iptables -A INPUT -i ens160 -p udp -m udp --dport 8443 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT
iptables -A INPUT -i ens192  -j ACCEPT
iptables -P INPUT DROP
iptables -t nat -A PREROUTING -p tcp -m tcp -d 10.0.141.1 --dport 443 -j DNAT --to-destination 10.24.184.179:443
#iptables -t nat -A PREROUTING -p udp -m udp -d 10.0.141.1 --dport 53 -j DNAT --to-destination 10.24.214.124:53

Il server DNS per me funge da archivio per i collegamenti IP-nome utente e non solo.
Darò anche qui la configurazione del mio server PowerDNS. Dirò solo che in termini di sicurezza, non è molto ottimizzato, sarebbe possibile consentire solo gli indirizzi corrispondenti, ma anche completamente funzionanti. L'ho copiato dal server funzionante, sostituendo solo i login/password/indirizzi con quelli falsi.
Configurazione del mio server autorevole PowerDNS
lancio = gmysql
gmysql-host = 10.24.214.131
gmysql-user = powerdns
gmysql-password = password
gmysql-dbname = powerdns
gmysql-dnssec = sì
allow-axfr-ips = 0.0.0.0 / 0
allow-dnsupdate-from = 0.0.0.0 / 0
allow-notify-from = 0.0.0.0 / 0
api = si
chiave api = 1234567890
# api-logfile = /var/log/pdns.log
api-sola lettura = no
cache-ttl = 2000
demone = si
default-soa-mail = dbuynovskiy.spectrum.by
disabilita-axfr = si
guardiano = si
indirizzo-locale = 0.0.0.0
porta locale = 53
log-dns-dettagli = sì
log-dns-queries = sì
funzione di registrazione = 0
loglevel = 7
maestro = si
dnsupdate = sì
max-tcp-connessioni = 512
ricevitori-thread = 4
thread di recupero = 4
riutilizzo = sì
setgid = pdns
setuid = pdns
thread di firma = 8
schiavo = no
intervallo di ciclo slave = 60
stringa-versione = powerdns
server web = sì
indirizzo-server web = 0.0.0.0
webserver-allow-from = 0.0.0.0 / 0,10.10.71.0 / 24,10.10.55.4 / 32
password-server web = 1234567890
porta del server web = 7777
webserver-print-arguments = sì
write-pid = yes
Ho usato l'API REST integrata per aggiornare le zone PowerDNS. Ho scritto la seguente procedura in Ruby:
Metodo per aggiornare la zona su PowerDNS tramite REST
  def updatezone(username,ipaddress)
    p data = {"rrsets":  [  {"name":  "#{username}.spectrum.loc.", "type":  "A", "ttl":  86400, "changetype":  "REPLACE",  "records":   [  {"content":  ipaddress, "disabled":  false }  ]  }  ]  }

    url = 'http://10.24.214.124:7777/api/v1/servers/localhost/zones/spectrum.loc.'
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    req = Net::HTTP::Patch.new(uri.request_uri)
    req["X-API-Key"]="1234567890"
    req.body = data.to_json
    p "fd"
    p response = http.request(req)
    p content = response.body
  end
Questo è il metodo che eseguo all'avvio del client. Cioè, c'è un aggiornamento dell'indirizzo IP corrispondente a un utente specifico. Lascia che ti ricordi che SOAP Gateway, scritto in Ruby, esegue tali richieste per me.
Le informazioni trasmesse tra i socket sono crittografate utilizzando l'algoritmo AES, ma non con una password separata, ma con una password selezionata casualmente dall'elenco, che fornisce una protezione quasi assoluta, anche se l'attaccante dispone di infinite risorse di calcolo. Ovviamente più lunga è la lista, meglio è.
Ho un metodo per eseguire questo tipo di crittografia/decrittografia.
Inoltre, voglio lasciare qui un esempio della mia procedura in c# per effettuare richieste SOAP.
Forse qualcuno tornerà utile. In ogni caso, è da tempo che mi impegno affinché le richieste SOAP vengano eseguite su piattaforme diverse. All'inizio ho usato il riferimento al servizio su Windows, ma per Xamarin per altre piattaforme non c'è. E questa tecnica funziona ovunque. Comunque, l'ho testato su IOS, Android e Windows
Un esempio di esecuzione di una richiesta SOAP in c#
       public static void registerSession2()
        {
            try
            {
                CallWebServiceUpdateLocation();
                writeLOG("Session registered on SoapGW.");
            }
            catch (Exception exc)
            {
                writeLOG("Error. Failed GlobalFunction.registerSession2() method:  " + Convert.ToString(exc));
            }
        }

        private static HttpWebRequest CreateWebRequest(string url, string action)
        {
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
            webRequest.Headers.Add("SOAPAction", action);
            webRequest.ContentType = "text/xml;charset=\"utf-8\"";
            webRequest.Accept = "text/xml";
            webRequest.Method = "POST";
            return webRequest;
        }

        private static XmlDocument CreateSoapEnvelope()
        {
            XmlDocument soapEnvelopeDocument = new XmlDocument();

            string xml = System.String.Format(@"

<soapenv:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soapenv=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns:spec=""https://spectrum.master"">
<soapenv:Header/>
<soapenv:Body>
<master_update_location soapenv:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/"">
<ipaddress xsi:type=""xsd:string"">{0}</ipaddress>
<username xsi:type=""xsd:string"">{1}</username>
</master_update_location>
</soapenv:Body>
</soapenv:Envelope>

", Convert.ToString(Globals.localip), Globals._LocalUserName);

            soapEnvelopeDocument.LoadXml(xml);
            return soapEnvelopeDocument;
        }

        public static void CallWebServiceUpdateLocation()
        {
            var _url = "http://10.0.141.1/master/action";
            var _action = "master_update_location";

            XmlDocument soapEnvelopeXml = CreateSoapEnvelope();
            HttpWebRequest webRequest = CreateWebRequest(_url, _action);
            InsertSoapEnvelopeIntoWebRequest(soapEnvelopeXml, webRequest);

            // begin async call to web request.
            IAsyncResult asyncResult = webRequest.BeginGetResponse(null, null);

            // suspend this thread until call is complete. You might want to
            // do something usefull here like update your UI.
            asyncResult.AsyncWaitHandle.WaitOne();

            // get the response from the completed web request.
            string soapResult;
            using (WebResponse webResponse = webRequest.EndGetResponse(asyncResult))
            {
                using (StreamReader rd = new StreamReader(webResponse.GetResponseStream()))
                {
                    soapResult = rd.ReadToEnd();
                }
                //Console.Write(soapResult);
            }
        }

        private static void InsertSoapEnvelopeIntoWebRequest(XmlDocument soapEnvelopeXml, HttpWebRequest webRequest)
        {
            using (Stream stream = webRequest.GetRequestStream())
            {
                soapEnvelopeXml.Save(stream);
            }
        }
PS Grazie a tutti per l'attenzione! Spero che qualcuno lo troverà utile, se non un'idea, almeno esempi di procedure. Fateci sapere nei commenti le vostre opinioni su questa implementazione di messaggistica.
Un po' di letteratura:
1. Articolo utile. L'ho usato io stesso. È vero, la configurazione proposta in esso non mi andava bene, ma il processo di installazione è molto dettagliato - Configurazione di un server OpenVPN su Ubuntu 16.04
2. Installazione di PowerDNS
3. Maggiori informazioni su Xamarin